├── .gitattributes ├── .gitignore ├── FileSaver.js ├── README.md ├── about.html ├── database ├── alias.json ├── conflicts.txt ├── crash_models.txt ├── groups.json ├── hashes.json ├── model_names.txt ├── models.json ├── tags.json └── versions.json ├── download.html ├── flamingline.gif ├── git_init.py ├── hlms ├── hlms.js ├── hlms.wasm ├── index.html ├── jszip.min.js ├── modeldb.js ├── modelguy └── scmodels.py /.gitattributes: -------------------------------------------------------------------------------- 1 | models.json binary 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | models/ 3 | .git_data 4 | access_token.txt 5 | updated.txt 6 | models.txt 7 | .nojekyll 8 | sound 9 | .git_snd 10 | zip_latest.txt 11 | install 12 | 13 | -------------------------------------------------------------------------------- /FileSaver.js: -------------------------------------------------------------------------------- 1 | /*! FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 2014-01-24 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * License: X11/MIT 7 | * See LICENSE.md 8 | */ 9 | 10 | /*global self */ 11 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 12 | 13 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 14 | 15 | var saveAs = saveAs 16 | // IE 10+ (native saveAs) 17 | || (typeof navigator !== "undefined" && 18 | navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) 19 | // Everyone else 20 | || (function(view) { 21 | "use strict"; 22 | // IE <10 is explicitly unsupported 23 | if (typeof navigator !== "undefined" && 24 | /MSIE [1-9]\./.test(navigator.userAgent)) { 25 | return; 26 | } 27 | var 28 | doc = view.document 29 | // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet 30 | , get_URL = function() { 31 | return view.URL || view.webkitURL || view; 32 | } 33 | , URL = view.URL || view.webkitURL || view 34 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 35 | , can_use_save_link = !view.externalHost && "download" in save_link 36 | , click = function(node) { 37 | var event = doc.createEvent("MouseEvents"); 38 | event.initMouseEvent( 39 | "click", true, false, view, 0, 0, 0, 0, 0 40 | , false, false, false, false, 0, null 41 | ); 42 | node.dispatchEvent(event); 43 | } 44 | , webkit_req_fs = view.webkitRequestFileSystem 45 | , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem 46 | , throw_outside = function(ex) { 47 | (view.setImmediate || view.setTimeout)(function() { 48 | throw ex; 49 | }, 0); 50 | } 51 | , force_saveable_type = "application/octet-stream" 52 | , fs_min_size = 0 53 | , deletion_queue = [] 54 | , process_deletion_queue = function() { 55 | var i = deletion_queue.length; 56 | while (i--) { 57 | var file = deletion_queue[i]; 58 | if (typeof file === "string") { // file is an object URL 59 | URL.revokeObjectURL(file); 60 | } else { // file is a File 61 | file.remove(); 62 | } 63 | } 64 | deletion_queue.length = 0; // clear queue 65 | } 66 | , dispatch = function(filesaver, event_types, event) { 67 | event_types = [].concat(event_types); 68 | var i = event_types.length; 69 | while (i--) { 70 | var listener = filesaver["on" + event_types[i]]; 71 | if (typeof listener === "function") { 72 | try { 73 | listener.call(filesaver, event || filesaver); 74 | } catch (ex) { 75 | throw_outside(ex); 76 | } 77 | } 78 | } 79 | } 80 | , FileSaver = function(blob, name) { 81 | // First try a.download, then web filesystem, then object URLs 82 | var 83 | filesaver = this 84 | , type = blob.type 85 | , blob_changed = false 86 | , object_url 87 | , target_view 88 | , get_object_url = function() { 89 | var object_url = get_URL().createObjectURL(blob); 90 | deletion_queue.push(object_url); 91 | return object_url; 92 | } 93 | , dispatch_all = function() { 94 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 95 | } 96 | // on any filesys errors revert to saving with object URLs 97 | , fs_error = function() { 98 | // don't create more object URLs than needed 99 | if (blob_changed || !object_url) { 100 | object_url = get_object_url(blob); 101 | } 102 | if (target_view) { 103 | target_view.location.href = object_url; 104 | } else { 105 | window.open(object_url, "_blank"); 106 | } 107 | filesaver.readyState = filesaver.DONE; 108 | dispatch_all(); 109 | } 110 | , abortable = function(func) { 111 | return function() { 112 | if (filesaver.readyState !== filesaver.DONE) { 113 | return func.apply(this, arguments); 114 | } 115 | }; 116 | } 117 | , create_if_not_found = {create: true, exclusive: false} 118 | , slice 119 | ; 120 | filesaver.readyState = filesaver.INIT; 121 | if (!name) { 122 | name = "download"; 123 | } 124 | if (can_use_save_link) { 125 | object_url = get_object_url(blob); 126 | // FF for Android has a nasty garbage collection mechanism 127 | // that turns all objects that are not pure javascript into 'deadObject' 128 | // this means `doc` and `save_link` are unusable and need to be recreated 129 | // `view` is usable though: 130 | doc = view.document; 131 | save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"); 132 | save_link.href = object_url; 133 | save_link.download = name; 134 | var event = doc.createEvent("MouseEvents"); 135 | event.initMouseEvent( 136 | "click", true, false, view, 0, 0, 0, 0, 0 137 | , false, false, false, false, 0, null 138 | ); 139 | save_link.dispatchEvent(event); 140 | filesaver.readyState = filesaver.DONE; 141 | dispatch_all(); 142 | return; 143 | } 144 | // Object and web filesystem URLs have a problem saving in Google Chrome when 145 | // viewed in a tab, so I force save with application/octet-stream 146 | // http://code.google.com/p/chromium/issues/detail?id=91158 147 | if (view.chrome && type && type !== force_saveable_type) { 148 | slice = blob.slice || blob.webkitSlice; 149 | blob = slice.call(blob, 0, blob.size, force_saveable_type); 150 | blob_changed = true; 151 | } 152 | // Since I can't be sure that the guessed media type will trigger a download 153 | // in WebKit, I append .download to the filename. 154 | // https://bugs.webkit.org/show_bug.cgi?id=65440 155 | if (webkit_req_fs && name !== "download") { 156 | name += ".download"; 157 | } 158 | if (type === force_saveable_type || webkit_req_fs) { 159 | target_view = view; 160 | } 161 | if (!req_fs) { 162 | fs_error(); 163 | return; 164 | } 165 | fs_min_size += blob.size; 166 | req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { 167 | fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { 168 | var save = function() { 169 | dir.getFile(name, create_if_not_found, abortable(function(file) { 170 | file.createWriter(abortable(function(writer) { 171 | writer.onwriteend = function(event) { 172 | target_view.location.href = file.toURL(); 173 | deletion_queue.push(file); 174 | filesaver.readyState = filesaver.DONE; 175 | dispatch(filesaver, "writeend", event); 176 | }; 177 | writer.onerror = function() { 178 | var error = writer.error; 179 | if (error.code !== error.ABORT_ERR) { 180 | fs_error(); 181 | } 182 | }; 183 | "writestart progress write abort".split(" ").forEach(function(event) { 184 | writer["on" + event] = filesaver["on" + event]; 185 | }); 186 | writer.write(blob); 187 | filesaver.abort = function() { 188 | writer.abort(); 189 | filesaver.readyState = filesaver.DONE; 190 | }; 191 | filesaver.readyState = filesaver.WRITING; 192 | }), fs_error); 193 | }), fs_error); 194 | }; 195 | dir.getFile(name, {create: false}, abortable(function(file) { 196 | // delete file if it already exists 197 | file.remove(); 198 | save(); 199 | }), abortable(function(ex) { 200 | if (ex.code === ex.NOT_FOUND_ERR) { 201 | save(); 202 | } else { 203 | fs_error(); 204 | } 205 | })); 206 | }), fs_error); 207 | }), fs_error); 208 | } 209 | , FS_proto = FileSaver.prototype 210 | , saveAs = function(blob, name) { 211 | return new FileSaver(blob, name); 212 | } 213 | ; 214 | FS_proto.abort = function() { 215 | var filesaver = this; 216 | filesaver.readyState = filesaver.DONE; 217 | dispatch(filesaver, "abort"); 218 | }; 219 | FS_proto.readyState = FS_proto.INIT = 0; 220 | FS_proto.WRITING = 1; 221 | FS_proto.DONE = 2; 222 | 223 | FS_proto.error = 224 | FS_proto.onwritestart = 225 | FS_proto.onprogress = 226 | FS_proto.onwrite = 227 | FS_proto.onabort = 228 | FS_proto.onerror = 229 | FS_proto.onwriteend = 230 | null; 231 | 232 | view.addEventListener("unload", process_deletion_queue, false); 233 | saveAs.unload = function() { 234 | process_deletion_queue(); 235 | view.removeEventListener("unload", process_deletion_queue, false); 236 | }; 237 | return saveAs; 238 | }( 239 | typeof self !== "undefined" && self 240 | || typeof window !== "undefined" && window 241 | || this.content 242 | )); 243 | // `self` is undefined in Firefox for Android content script context 244 | // while `this` is nsIContentFrameMessageManager 245 | // with an attribute `content` that corresponds to the window 246 | 247 | if (typeof module !== "undefined") module.exports = saveAs; 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scmodels 2 | 3 | A faster way to find player models. 4 | 5 | Website: 6 | https://wootguy.github.io/scmodels/ 7 | 8 | Model repos: 9 | https://github.com/wootdata?tab=repositories 10 | 11 | Model viewer and thumbnail generator (hlms): 12 | https://github.com/wootguy/HL_Tools 13 | 14 | Model JSON exporter: 15 | https://github.com/wootguy/modelguy 16 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sven Co-op Model Database 8 | 9 | 41 | 42 | 43 |

Sven Co-op Model Database

44 | 49 |
50 | 51 | 52 |
53 |

About

54 |

55 | The in-game model browser is buggy and hard to use. 56 | Use this site instead to find a model, then use it in game 57 | by typing a model command in console (example: "model betagordon"). 58 | You can click on a model's name to copy its name to the clipboard. 59 |

60 | 61 |

62 | This site is a combination of various packs and single releases. There are often conflicts where models 63 | have the same names. I try to add version suffixes properly but might not always get that right. 64 | If I can't compare modified dates, then I check the animations to see which version should be the latest. 65 |

66 | 67 |

68 | Source code for this site 69 |

70 | 71 |

Model names

72 |

73 | People often copy-paste models and then rename them. These renamed models then get distributed in model packs. 74 | Because of this, I don't know if any of the model names here are correct. Let me know 75 | if a model name is wrong and I'll fix it (preferably with some proof like a GameBanana link). 76 |

77 | 78 |

79 | Some recently released models have names that conflict with older models. I renamed those models. 80 | For instance, this Touhou Momiji model overwrites 81 | the low definition version of that model. So, I renamed the new one to "kz_touhou_momiji". 82 | There are many other cases like this. 83 | You can also see previous model names in the "Known Aliases" section of the model viewer. 84 |

85 | 86 |

87 | Version suffixes (e.g. "_v2") are added and necessary because servers can't overwrite existing models in your model 88 | folder. If a server wants to automatically distribute an updated model, then that model has to have a 89 | new name or else many players won't see it. 90 |

91 |

92 | Model names are also lowercased so that they can easily be served on Linux FastDL 93 | servers, and to simplify plugin/website logic. That isn't totally necessary and I regret doing it. Some 94 | model names used capital letters to separate names and acronyms. Now it might be harder to figure out 95 | where those models came from if you aren't familiar with every game/movie/anime/etc. 96 |

97 | 98 |

Model sources

99 |

100 |

110 |

111 |
112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /database/conflicts.txt: -------------------------------------------------------------------------------- 1 | Models that I renamed because an older model had the same name: 2 | elvis -> pd_elvis 3 | simon -> 7ds_simon 4 | cof_simon -> cof_simon_v2 5 | jill -> jill_grey 6 | Touhou_Momiji -> kz_touhou_momiji 7 | Venom -> VenomReal_v2 8 | warrior_skeleton -> warrior_skeleton_2 9 | Zombie2 -> th_zombie2 10 | pyro -> pyro_hecu 11 | cryokeen -> cryokeen_hd 12 | demon -> demon2 13 | grunt -> grunt2 14 | hgrunt_hd -> hgrunt_hd_v2 15 | hgrunt -> hgrunt_hd 16 | vamp -> vamp2 17 | alien -> alien2 18 | avp2pred -> avp2pred2 19 | crash_bandicoot -> crash_bandicoot2 20 | fishman -> fishman2 21 | hatsunemiku_10a -> hatsunemiku_10a2 22 | james -> james2 23 | kancolle_harbour -> kancolle_harbour2 24 | shadow1 -> shadow2_v2 25 | tails -> tails_ld 26 | vocaloid_miku -> vocaloid_miku2 27 | billy -> billy2_v2 28 | kf_greywolf -> kf_greywolf_kz 29 | gi_ganyu -> gi_ganyu_hz 30 | dawae/lt_uganda/uganda* -> uganda_knuckes_voice 31 | azur_lane_enterprise -> al_enterprise_v1 32 | sa_cj -> sa_cj2 33 | op4_shephard -> bms_shephard 34 | dmc_vergil -> dmc_vergil2 35 | wireframe -> wireframe2 36 | natasha -> q2_natasha 37 | pokerface -> pokerface2 38 | dm_helmet -> dm_helmet_white 39 | rat -> rat2 40 | shotgun -> weapon_shotgun 41 | puro -> puro2 42 | steve -> steve2 43 | hl_grunt -> hl_grunt3 44 | hl_recon -> hl_recon2 45 | hl_sarge -> hl_sarge2 46 | hl_shotgun -> hl_shotgun3 47 | dragon -> dragon2 48 | 49 | added _v2 to some models from snark. Once a model is distributed by a server, 50 | that name should never be used again, or else clients won't get the updated file. 51 | 52 | added _v2 to some recent models that already existed in the 2017 version of the 53 | TWLZ player model pack. Sometimes _v3 and _v4 were added to match chronological 54 | dates of the models, meaning the original name isn't used anymore because I only 55 | know for sure that the _v2 version is correctly named (e.g. rouge -> rouge_v3 instead 56 | of swapping rouge_v2 and rouge). 57 | 58 | added _v2 to models that were updated on GameBanana without being renamed. 59 | 60 | added "_ld" to all player models from this pack: 61 | https://gamebanana.com/mods/167285 62 | They overwrite the default player models otherwise. 63 | 64 | added "_up" to the upscaled texture they hunger pack 65 | https://www.moddb.com/mods/sven-co-op/addons/they-hunger-upscale-ld-quality 66 | 67 | added "rvi_" to BrussTrigger's Revitalization Playermodels 68 | 69 | Model names that were shortened because they were over the 22 character limit (can't be 70 | selected or downloaded from a server). Most of these are my fault (version suffixes): 71 | 2d_hdn_neppugia-cosplay -> 2d_hdn_neppugia-cos 72 | 2d_hdn_neptune-mizugi-c2 -> 2d_hdn_neptune-miz-c2 73 | 2d_kancolle_shimakaze_v2 -> 2d_kc_shimakaze_v2 74 | 2d_mdn_tenno_uzume-mizugi -> 2d_mdn_tenno-mizugi 75 | 2d_mdn_tennouboshi_uzume -> 2d_mdn_tenno_uzume 76 | 2d_touhou_flandre_scarlet -> 2d_touhou_scarlet 77 | 2d_touhou_izayoi_sakuya -> 2d_touhou_sakuya 78 | 2d_touhou_kazami_yuuka_4 -> 2d_touhou_yuuka_4 79 | 2d_touhou_konpaku_youmu -> 2d_touhou_youmu 80 | 2d_touhou_suikaabcedition -> 2d_touhou_suikaabc 81 | azur_lane_enterprise_v2 -> al_enterprise2_v2 82 | bs_unarmored_barney_1_ld -> bs_barney_1_ld 83 | bs_unarmored_barney_2_ld -> bs_barney_2_ld 84 | bwtgs_sena_kashiwazaki_v2 -> bwtgs_sena_v2 85 | cof_simon_leatherhoff_v2 -> cof_simon_l_v2 86 | cof_simon_psykskallar_v2 -> cof_simon_p_v2 87 | cyborg_ninja_liquidheat -> cyborg_ninja_heat 88 | david_leatherhoff_female -> david_l_female 89 | furry_maid_black2_noglow -> furry_maid_black3 90 | hdn_neppugia-cosplay_v2 -> hdn_neppugia-cos_v2 91 | hdn_neptune-mizugi-c2_v2 -> hdn_neptune-miz-c2_v2 92 | hl2_combine_soldier_prisonguard -> hl2_prisonguard 93 | infected_businessman_v2 -> infected_bman_v2 94 | kfparamedicalfredanderson -> kfparamedic 95 | monogatari_hanekawa_0b_v2 -> mg_hanekawa_0b_v2 96 | monogatari_hanekawa_1b_v2 -> mghanekawa_1b_v2 97 | monogatari_hanekawa_b_v2 -> mghanekawa_b_v2 98 | op4_scientist_einstein_ld -> op4_sci_einstein_ld 99 | op4_scientist_luther_ld -> op4_sci_luther_ld 100 | op4_scientist_walter_ld -> op4_sci_walter_ld 101 | player_multiplayer_model_st -> multiplayer_model_st 102 | re_succ_citizen_unit_v2 -> res_citizen_v2 103 | re_succ_combat_response_unit -> res_combat_resp 104 | re_succ_combat_technician -> res_combat_tech 105 | re_succ_commander_merc_v2 -> res_commander_merc_v2 106 | re_succ_corporal_gote_v2 -> res_corp_gote_v2 107 | re_succ_major_lie_rick_v2 -> res_major_lie_rick_v2 108 | re_succ_marksman_unit_v2 -> res_marksman_v2 109 | re_succ_operator_unit_v2 -> res_operator_v2 110 | re_succ_security_unit_v2 -> res_security_v2 111 | re_succ_security_unita_v2 -> res_securitya_v2 112 | re_succ_security_unitb_v2 -> res_securityb_v2 113 | re_succ_security_unitc_v2 -> res_securityc_v2 114 | rvi_bs_unarmored_barney_1 -> rvi_bs_barney_1 115 | rvi_bs_unarmored_barney_2 -> rvi_bs_barney_2 116 | sister_mary_ellie_fant_v2 -> mary_ellie_fant_v2 117 | sonic_forces_classic_c_v2 -> sonicf_classic_c_v2 118 | sonic_forces_classic_c_v3 -> sonicf_classic_c_v3 119 | sonic_forces_classic_v2 -> sonicf_classic_v2 120 | sonic_forces_classic_v3 -> sonicf_classic_v3 121 | spess_kagerou_sweater_v2 -> spess_kagerou2_v2 122 | stalker_freedomantigas_v2 -> stalker_freedomagas_v2 123 | stalker_lonerantigas_v2 -> stalker_loneragas_v2 124 | stalker_monolithantigas -> stalker_monoagas 125 | stalker_monolithseva_v2 -> stalker_monoseva_v2 126 | ta_c_elite_color_armor2 -> ta_c_elite_c_armor2 127 | ta_c_s_advisor_armor2_v2 -> ta_c_s_advisor3_v2 128 | ta_c_s_advisor_armor_v2 -> ta_c_s_advisor4_v2 129 | ta_c_s_arctic_armor2_v2 -> ta_c_s_arctic3_v2 130 | ta_c_s_arctic_armor_v2 -> ta_c_s_arctic4_v2 131 | ta_c_shadow-3_armor2_v2 -> ta_c_shadow-3_a2_v2 132 | tc_alternative-dr.kleiner -> tc_alt-dr.kleiner 133 | touhou_izayoi_sakuya_v2 -> -> touhou_sakuya_v2 134 | touhou_kagerou_bikini_v2 -> touhou_kagerou2_v2 135 | touhou_kochiya_sanae_v2 -> touhou_k_sanae_v2 136 | touhou_reisenudongeininaba -> touhou_reisen2 137 | touhou_yakumo_yukari_v2 -> touhou_yukari_v2 138 | vehicleshit_m1a1_abrams -> vehicleshit_abrams 139 | vinnie_gognitti_alt_clean -> vinnieg_alt_clean 140 | vinnie_gognitti_plain_clean -> vinnieg_plain_clean 141 | vocaloid_yukari-yuzuki_v2 -> vocaloid_yukari_v2 -------------------------------------------------------------------------------- /database/crash_models.txt: -------------------------------------------------------------------------------- 1 | axis2_s5 2 | tomb_rider 3 | white_suit 4 | axis2_s5_v2 5 | tomb_rider_v2 6 | white_suit_v2 7 | kz_rindo_swc 8 | vtuber_filian_sw 9 | lt_ugandamiku -------------------------------------------------------------------------------- /database/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "anime": [ 3 | "&uni_rejsyg", 4 | "-agent_onna-_v2", 5 | "-babygirl-_v2", 6 | "-darkfairy-_v2", 7 | "-diamond-", 8 | "-finallyitworks", 9 | "-finallyitworks2", 10 | "-finallyitworks3", 11 | "-finallyitworks4", 12 | "2d_ab_kanna_coco", 13 | "2d_ab_rin_shibuya", 14 | "2d_ab_sakura_kinomoto", 15 | "2d_bunny_gumi", 16 | "2d_bunny_ia", 17 | "2d_bunny_luka", 18 | "2d_bunny_meko", 19 | "2d_bunny_rin", 20 | "2d_bwtgs_sana", 21 | "2d_furry_maid_1", 22 | "2d_furry_maid_2", 23 | "2d_furry_maid_3", 24 | "2d_green_heart", 25 | "2d_gs_blanc1", 26 | "2d_gs_brs_1", 27 | "2d_gs_ethel_fairyf_v2", 28 | "2d_gs_grenheart", 29 | "2d_gs_keijinguji", 30 | "2d_gs_keiwaffenss", 31 | "2d_gs_miku_@w@", 32 | "2d_gs_mikupanda", 33 | "2d_gs_noire", 34 | "2d_gs_op_uni", 35 | "2d_gs_orangesister", 36 | "2d_gs_purplesister", 37 | "2d_gs_soldiergirl_v2", 38 | "2d_gs_whiteheart", 39 | "2d_gs_yuri_v5", 40 | "2d_hdn_blanc", 41 | "2d_hdn_blanc-mizugi", 42 | "2d_hdn_blanc-mizugi-c2", 43 | "2d_hdn_blanc-school", 44 | "2d_hdn_blancv", 45 | "2d_hdn_blancv-c2", 46 | "2d_hdn_blancv-c3", 47 | "2d_hdn_blancv-c4", 48 | "2d_hdn_compa", 49 | "2d_hdn_compa-c2", 50 | "2d_hdn_if", 51 | "2d_hdn_if-c2", 52 | "2d_hdn_nepgear", 53 | "2d_hdn_neppugia", 54 | "2d_hdn_neppugia-c2", 55 | "2d_hdn_neppugia-c3", 56 | "2d_hdn_neppugia-c4", 57 | "2d_hdn_neppugia-cos", 58 | "2d_hdn_neppugia-idol", 59 | "2d_hdn_neppugia-maid", 60 | "2d_hdn_neppugia-mizugi", 61 | "2d_hdn_neppugya", 62 | "2d_hdn_neptune", 63 | "2d_hdn_neptune-idol", 64 | "2d_hdn_neptune-mizugi", 65 | "2d_hdn_neptune-miz-c2", 66 | "2d_hdn_neptune-school", 67 | "2d_hdn_neptune_black", 68 | "2d_hdn_neptune_poritan", 69 | "2d_hdn_neptunev", 70 | "2d_hdn_neptunev-c2", 71 | "2d_hdn_neptunev-c3", 72 | "2d_hdn_neptunev-c4", 73 | "2d_hdn_plutia", 74 | "2d_hdn_plutia_c2", 75 | "2d_hdn_plutia_c3", 76 | "2d_hdn_plutia_c4", 77 | "2d_hdn_uni", 78 | "2d_hdnr_blanc-c2", 79 | "2d_hdnr_blanc-c3", 80 | "2d_hdnr_blanc-c4", 81 | "2d_hdnr_compa-c2", 82 | "2d_hdnr_neptune", 83 | "2d_hdnr_neptune-c2", 84 | "2d_hdnr_neptune-c3", 85 | "2d_helltaker_modeus", 86 | "2d_hn_purple_heart_a", 87 | "2d_hn_purple_heart_n", 88 | "2d_hn_uni_maid", 89 | "2d_kancolle_hibiki_n", 90 | "2d_kc_shimakaze_v2", 91 | "2d_kanna_christmas", 92 | "2d_kc_akatsuki", 93 | "2d_kc_amatsukaze", 94 | "2d_kc_atago", 95 | "2d_kc_atago_n", 96 | "2d_kc_bismarck_n", 97 | "2d_kc_destroyer", 98 | "2d_kc_harbour_v4", 99 | "2d_kc_hibiki_v2", 100 | "2d_kc_ikazuchi", 101 | "2d_kc_northern", 102 | "2d_kc_shimakaze", 103 | "2d_kc_shimakaze_v2_n", 104 | "2d_kf_greywolf", 105 | "2d_kf_serval", 106 | "2d_kf_shoebill", 107 | "2d_kf_tiger", 108 | "2d_kf_tsuchinoko", 109 | "2d_km_karenkujo", 110 | "2d_km_karenkujo_alt", 111 | "2d_kmd_kanna", 112 | "2d_kz_bowsette", 113 | "2d_kz_fgo_mheroine", 114 | "2d_kz_lolimiku", 115 | "2d_kz_marisa", 116 | "2d_kz_megumi_v2", 117 | "2d_kz_miku", 118 | "2d_kz_mikutda_v2", 119 | "2d_kz_moeglados_v2", 120 | "2d_kz_neruu", 121 | "2d_kz_op_evilmiku", 122 | "2d_kz_reimu", 123 | "2d_lt_bradpquito_sg", 124 | "2d_marisa", 125 | "2d_mc_astray", 126 | "2d_mce_ene", 127 | "2d_mdn_tenno_uzume-c2", 128 | "2d_mdn_tenno_uzume-c3", 129 | "2d_mdn_tenno-mizugi", 130 | "2d_mdn_tenno_uzume", 131 | "2d_momiji_inubashiri", 132 | "2d_monogatari_shinobu", 133 | "2d_nekopara_azuki", 134 | "2d_nekopara_chocola", 135 | "2d_nekopara_coconut", 136 | "2d_nekopara_vanilla", 137 | "2d_nepnep3_mazz", 138 | "2d_noire", 139 | "2d_noire_school", 140 | "2d_ooka_miko", 141 | "2d_ooka_miko_2nd", 142 | "2d_padoru_padoru", 143 | "2d_peashy", 144 | "2d_ram", 145 | "2d_red_fox", 146 | "2d_red_fox2", 147 | "2d_reisen", 148 | "2d_rezero_ram", 149 | "2d_rezero_rem", 150 | "2d_sp_arabianmiku", 151 | "2d_touhou_aya", 152 | "2d_touhou_aya2", 153 | "2d_touhou_aya2", 154 | "2d_touhou_chen", 155 | "2d_touhou_clownpiece", 156 | "2d_touhou_daiyousei", 157 | "2d_touhou_scarlet", 158 | "2d_touhou_inaba_tewi", 159 | "2d_touhou_sakuya", 160 | "2d_touhou_kagerou_v2", 161 | "2d_touhou_yuuka_4", 162 | "2d_touhou_koa_devil", 163 | "2d_touhou_koishi", 164 | "2d_touhou_koishi2", 165 | "2d_touhou_youmu", 166 | "2d_touhou_letty", 167 | "2d_touhou_patche", 168 | "2d_touhou_patche2", 169 | "2d_touhou_patchouli", 170 | "2d_touhou_reimu_mmd", 171 | "2d_touhou_rumia", 172 | "2d_touhou_suikaabc", 173 | "2d_touhou_suwakero", 174 | "2d_touhou_suwako", 175 | "2d_touhou_suwako2", 176 | "2d_touhou_tenshi_v2", 177 | "2d_touhou_yakumo_ran", 178 | "2d_yellow_heart", 179 | "2d_yorha_2b", 180 | "2d_ys_yandere_insane", 181 | "[darkan", 182 | "_nud_rejsyg", 183 | "_ph_rejsyg_v2", 184 | "_uni_rejsyg", 185 | "_wh_rejsyg_v2", 186 | "ab_kanna_coco", 187 | "ab_rei_ryghts_v2", 188 | "ab_rin_shibuya_kp", 189 | "ab_rin_shibuya_v2", 190 | "ab_sakura_kinomoto", 191 | "aco", 192 | "acr_angelina", 193 | "aegis", 194 | "afterlife_nyannyan", 195 | "ag_osiris", 196 | "ag_osiris_c", 197 | "ak_merilee_v2", 198 | "akyu", 199 | "al_akashi", 200 | "al_ayanami", 201 | "al_ayanami-c2", 202 | "al_ayanami_meranian", 203 | "al_enterprise_v2", 204 | "al_enterprise_v2", 205 | "al_meragato_v1", 206 | "al_nagato", 207 | "al_nagato_ld", 208 | "al_nagato_meranian", 209 | "alice2_hev", 210 | "alice2_hev_alt", 211 | "alice_nude", 212 | "alice_v2", 213 | "altyumi", 214 | "ami", 215 | "anime_sakura", 216 | "arche", 217 | "arknights_ansel_v2", 218 | "arknights_lappland", 219 | "arknights_lappland_v2", 220 | "arthur_pendragon", 221 | "artoria_pendragon", 222 | "as_jiaren_alpha", 223 | "astolfo_fumo", 224 | "astolfo_mazz", 225 | "astolfo_mazz_old", 226 | "astolfo_mera_dcf", 227 | "astolfo_mera_dcf2", 228 | "astolfo_mera_v2", 229 | "astolfo_ranian", 230 | "astolfo_xmas", 231 | "astolfo_yang_v1", 232 | "aya", 233 | "aya2", 234 | "aya_h", 235 | "ba_miyu", 236 | "ba_miyu_v2", 237 | "black-goku", 238 | "black_heart", 239 | "black_miku", 240 | "blanc_chl_srs", 241 | "blanc_kagu", 242 | "bluebikini", 243 | "bluebikini2", 244 | "bluebikini3", 245 | "bluecoat", 246 | "bluecoat2", 247 | "bluecoat3", 248 | "bluejacket", 249 | "bluepolice", 250 | "bluesexy", 251 | "bonniefnia", 252 | "broly", 253 | "brs", 254 | "brs@_@", 255 | "brs@_@2", 256 | "brs_dark_mecha", 257 | "brs_digitrevx-v2_v2", 258 | "brs_digitrevx2_v2", 259 | "brs_digitrevx_v2", 260 | "brs_kuroi_mato", 261 | "brs_male", 262 | "brs_ssj", 263 | "brs_white", 264 | "bruno_v2", 265 | "bunny_gumi_v2", 266 | "bunny_haku_v2", 267 | "bunny_ia_v2", 268 | "bunny_luka_v2", 269 | "bunny_meko_v2", 270 | "bunny_miku_v2", 271 | "bunny_nekomata", 272 | "bunny_rin_v2", 273 | "bunny_teto_v2", 274 | "bunny_yukari_v4", 275 | "bunnygirl_buruma_v2", 276 | "bunnygirl_casino1_v2", 277 | "bunnygirl_casino2_v2", 278 | "bunnygirl_pink_v2", 279 | "bunnygirl_space_v2", 280 | "bunnygirl_v2", 281 | "bunnygirl_white_v2", 282 | "bwtgs_sena_v2", 283 | "ccsakura_v2", 284 | "cell", 285 | "chammy_xrs1", 286 | "chen", 287 | "chibi_wattson", 288 | "chibireimu", 289 | "chicafnia", 290 | "chichi", 291 | "cirno2", 292 | "cirno_1", 293 | "cirno_2", 294 | "cirno_3", 295 | "cirno_v2", 296 | "closers_tina", 297 | "closers_tina-c2", 298 | "closers_tina-ss1", 299 | "closers_tina-ss2", 300 | "closers_tina-ss3", 301 | "cm3d2_catgirl", 302 | "cm3d2_dragon", 303 | "cm3d2_fox", 304 | "cm3d2_kagerou2", 305 | "cm3d2_miku_bunny", 306 | "cm3d2_succubus_1", 307 | "cm3d2_succubus_2", 308 | "coconut", 309 | "colt45beer", 310 | "comet", 311 | "conan", 312 | "conanbig", 313 | "cooking_mama", 314 | "corona_chan", 315 | "corona_chan_merania", 316 | "corona_chan_meranian", 317 | "corona_chan_nopantsu", 318 | "corona_chan_xmas", 319 | "covid_nagato_meranian", 320 | "cribbon3", 321 | "cribbon3.0", 322 | "cybertrooper_v3", 323 | "cyberzuna", 324 | "cyberzuna_c", 325 | "d4miku", 326 | "da_blackgoku", 327 | "da_blackgoku_rose", 328 | "da_broly", 329 | "da_cell", 330 | "da_freezer", 331 | "da_gohan", 332 | "da_gohan_ssj", 333 | "da_gohan_ssj2", 334 | "da_goku", 335 | "da_goku_ssj", 336 | "da_goku_ssj3", 337 | "da_goku_ssj4", 338 | "da_goku_ssjblue", 339 | "da_golden_freezer", 340 | "da_hit", 341 | "da_ikaros", 342 | "da_jaco", 343 | "da_jeice", 344 | "da_kaito_shion", 345 | "da_kyabe", 346 | "da_len_kagamine", 347 | "da_magetta", 348 | "da_roshi", 349 | "da_sauza", 350 | "da_tarles", 351 | "da_trunks", 352 | "da_trunks_ssj", 353 | "da_vegeta_ssj4", 354 | "da_vegetafnf", 355 | "da_yamcha", 356 | "da_zamasu", 357 | "daiyousei", 358 | "dangeroustoy2", 359 | "dangeroustoy3", 360 | "david_saiyajin", 361 | "david_skin", 362 | "dbg_gokusjj5", 363 | "dbg_vegetasjj5", 364 | "dbztrunks", 365 | "ddlc_monika", 366 | "ddlc_natsuki", 367 | "ddlc_sayori", 368 | "ddlc_yuri", 369 | "deathscythehell", 370 | "deathscythehell2", 371 | "decepticon", 372 | "dende", 373 | "diamond", 374 | "dm_chan", 375 | "dmz_brolly", 376 | "dmz_buu", 377 | "dmz_cell", 378 | "dmz_cell2", 379 | "dmz_frieza", 380 | "dmz_gohan", 381 | "dmz_goku", 382 | "dmz_goku1", 383 | "dmz_krillin", 384 | "dmz_perfectcell", 385 | "dmz_piccolo", 386 | "dmz_ssjgohan", 387 | "dmz_ssjgoku", 388 | "dmz_ssjtrunks", 389 | "dmz_ssjvegeta", 390 | "dmz_trunks", 391 | "dmz_vegeta", 392 | "doggy_sakuya", 393 | "dora", 394 | "doraemon", 395 | "dotso_erio_touwa", 396 | "dotso_erio_v2", 397 | "dunbine", 398 | "eirin", 399 | "eirin_blue", 400 | "eirin_dm", 401 | "eirin_red", 402 | "elfenlied_lucy", 403 | "enigma", 404 | "esf_armoredtrunks", 405 | "esf_armoredtrunksssj", 406 | "esf_bardock", 407 | "esf_cell", 408 | "esf_cell2", 409 | "esf_cell3_v2", 410 | "esf_dmgbardock", 411 | "esf_evilbuu", 412 | "esf_frieza2", 413 | "esf_frieze2", 414 | "esf_goku", 415 | "esf_gokussj", 416 | "esf_piccolo", 417 | "esf_suitvegeta", 418 | "esf_teentrunks", 419 | "esf_trunks", 420 | "esf_trunksssj", 421 | "esf_vegeta", 422 | "esf_vegeta1", 423 | "esf_vegetassj", 424 | "eva", 425 | "evil_goku", 426 | "evil_goku_sjj3", 427 | "f91", 428 | "fate_jeanne", 429 | "fate_jeanne_alt1", 430 | "fate_jeanne_alt2", 431 | "fate_jeanne_alt3", 432 | "felix_argyle", 433 | "ferin", 434 | "fering", 435 | "fetaiga", 436 | "fgo_astolfo-sf", 437 | "fgo_astolfo-sf2", 438 | "fgo_astolfo_black", 439 | "fkotomine", 440 | "flandre", 441 | "flandre2", 442 | "flandre3", 443 | "flandre4_v2", 444 | "flandre5", 445 | "flandre_bikini", 446 | "flandre_cab_nude", 447 | "flandre_nude", 448 | "flandre_scarletv1", 449 | "flunder", 450 | "flunder1", 451 | "fmash", 452 | "fmordred", 453 | "fp_chupetin", 454 | "fp_david2017", 455 | "fp_greth2017", 456 | "fp_mikito2016", 457 | "fp_mikito2018", 458 | "fp_miku2018", 459 | "fredina", 460 | "freedom", 461 | "freedom2", 462 | "freezer", 463 | "frieza", 464 | "fscathach_v2", 465 | "fsn_saber", 466 | "fsnsaber(out)", 467 | "fumolarge", 468 | "furry_maid_1_v2", 469 | "furry_maid_2_v2", 470 | "furry_maid_3_v2", 471 | "furry_maid_black", 472 | "furry_maid_black2", 473 | "furry_maid_black3", 474 | "furry_maid_blackv2", 475 | "furry_maid_kubo", 476 | "furry_maid_viny", 477 | "furry_maid_vulpea", 478 | "fuuko_v2", 479 | "g_gintoki", 480 | "gaara", 481 | "gaara_16", 482 | "gakusei_hk416", 483 | "galgun_akira", 484 | "gf_9a-91_smol_v2", 485 | "gf_9a-91_v2", 486 | "gf_g11", 487 | "gf_idw_v3", 488 | "gf_idw_v3", 489 | "gf_ntw-20", 490 | "gf_ntw-20_bikini", 491 | "gf_ntw-20_bikini_g", 492 | "gf_ntw-20_bunny", 493 | "gf_ntw-20_bunny_g", 494 | "gf_ntw-20_g", 495 | "gf_ntw-20_lingerie", 496 | "gf_ntw-20_lingerie_g", 497 | "gf_sass_lingerie_1", 498 | "gf_sass_lingerie_2", 499 | "gf_super_sass", 500 | "gf_super_sass_a", 501 | "gf_super_sass_bikini", 502 | "gf_super_sass_bunny", 503 | "gf_super_sass_g", 504 | "gf_super_sass_nude_1", 505 | "gf_super_sass_nude_2", 506 | "gf_ump40", 507 | "gf_ump45", 508 | "gf_ump9", 509 | "gfl_ak12", 510 | "gfl_ak12_alt1", 511 | "gfl_ak12_alt2", 512 | "gfl_hk416_hatless", 513 | "gfl_hk416_v3", 514 | "gfl_m14-c2_v2", 515 | "gfl_m14_v2", 516 | "gfl_m4_sopmod", 517 | "gfl_m4_sopmod_c2", 518 | "gfl_m4a1", 519 | "gfl_m4a1_dark", 520 | "gfl_negev", 521 | "gfl_negev_shoeless", 522 | "gfl_ump45", 523 | "gfl_ump45-c2", 524 | "gfl_ump9", 525 | "gfl_ump9-c2", 526 | "gi_aether", 527 | "gi_amber", 528 | "gi_ayaka_v3", 529 | "gi_barbara", 530 | "gi_diona", 531 | "gi_diona_meranian", 532 | "gi_ganyu", 533 | "gi_ganyu_dark", 534 | "gi_ganyu_hz", 535 | "gi_ganyu_hz_alt", 536 | "gi_hutao", 537 | "gi_keqing_v2", 538 | "gi_klee_v2", 539 | "gi_kukishinobu", 540 | "gi_kukishinobu_nm", 541 | "gi_lumine", 542 | "gi_lumine_abyss_order", 543 | "gi_lumine_dark", 544 | "gi_mona", 545 | "gi_mona_v2", 546 | "gi_mona_v3", 547 | "gi_mona_v4", 548 | "gi_paimon", 549 | "gi_qiqi", 550 | "gi_sayu", 551 | "gi_sucrose", 552 | "gi_yaemiko", 553 | "gi_yanfei", 554 | "gi_yoimiya", 555 | "gigacirno_v2", 556 | "giorno", 557 | "girlz_v2", 558 | "glados barney", 559 | "glados_wip", 560 | "gm_ilulu_v1", 561 | "god", 562 | "gohan", 563 | "gohan_ad", 564 | "gohan_ssj", 565 | "gohan_ssj2", 566 | "goku", 567 | "goku_sjj7", 568 | "goku_ssj", 569 | "goku_ssj2", 570 | "goku_ssj3", 571 | "goku_ssj4", 572 | "goku_ultra_instinct", 573 | "gokufnf", 574 | "gokussgss", 575 | "gokussj_mystic4", 576 | "golden_freezer", 577 | "goldie", 578 | "gothiccatlady_v2", 579 | "gp01fb", 580 | "greatmazinger", 581 | "green_heart", 582 | "greendam", 583 | "gs_blanc1", 584 | "gs_brs", 585 | "gs_brs_1", 586 | "gs_brs_@w@", 587 | "gs_ethel_fairyf", 588 | "gs_ethel_fairyfencer", 589 | "gs_grenheart", 590 | "gs_hakuyowane", 591 | "gs_kasaneteto", 592 | "gs_keijinguji", 593 | "gs_keiwaffenss", 594 | "gs_miku", 595 | "gs_miku_1", 596 | "gs_miku_v3", 597 | "gs_mikupanda_teq", 598 | "gs_moeglados", 599 | "gs_op_evilmiku", 600 | "gs_op_hpgirl", 601 | "gs_op_kazami", 602 | "gs_op_lucy", 603 | "gs_op_uni", 604 | "gs_orangesister", 605 | "gs_rabbitgirl", 606 | "gs_soldiergirl_v2", 607 | "gs_whiteheart", 608 | "gundam", 609 | "gundam_rx-78", 610 | "gx", 611 | "h-mdn_anko_kurome", 612 | "h-mdn_tenno_uzume", 613 | "hachikuji_v1", 614 | "haruhi", 615 | "haruhi_azusa", 616 | "haruhi_azusa2", 617 | "haruhi_haruhi", 618 | "haruhi_mio", 619 | "haruhi_ritu", 620 | "haruhi_yui", 621 | "hatsune-mikuver2", 622 | "hatsunemiku1", 623 | "hatsunemiku_10a2", 624 | "hatsunemiku_10a_v1", 625 | "hatsunemiku_v2", 626 | "hdn_blanc", 627 | "hdn_blanc-mizugi", 628 | "hdn_blanc-mizugi-c2", 629 | "hdn_blanc-school", 630 | "hdn_blancv", 631 | "hdn_blancv-c2", 632 | "hdn_blancv-c3", 633 | "hdn_blancv-c4", 634 | "hdn_compa-c2_v2", 635 | "hdn_compa_v2", 636 | "hdn_if", 637 | "hdn_if-c2", 638 | "hdn_nepgear", 639 | "hdn_neppugia-c2", 640 | "hdn_neppugia-c3", 641 | "hdn_neppugia-c4_v2", 642 | "hdn_neppugia-cos_v2", 643 | "hdn_neppugia-idol_v2", 644 | "hdn_neppugia-maid_v2", 645 | "hdn_neppugia-mizugi_v2", 646 | "hdn_neppugia_v2", 647 | "hdn_neppugya-c2", 648 | "hdn_neppugya-c3", 649 | "hdn_neppugya-c4", 650 | "hdn_neppugya-cosplay", 651 | "hdn_neppugya-idol", 652 | "hdn_neppugya-maid", 653 | "hdn_neppugya-mizugi", 654 | "hdn_neppugya_v2", 655 | "hdn_neptune-idol_v2", 656 | "hdn_neptune-miz-c2_v2", 657 | "hdn_neptune-mizugi_v2", 658 | "hdn_neptune-school_v2", 659 | "hdn_neptune_black_v2", 660 | "hdn_neptune_v2", 661 | "hdn_neptunev-c2_v2", 662 | "hdn_neptunev-c3_v2", 663 | "hdn_neptunev-c4_v2", 664 | "hdn_neptunev_v2", 665 | "hdn_noire-vgen", 666 | "hdn_plutia", 667 | "hdn_plutia_c2", 668 | "hdn_plutia_c3", 669 | "hdn_plutia_c4", 670 | "hdn_uni", 671 | "hdnr_blanc", 672 | "hdnr_blanc-c2", 673 | "hdnr_blanc-c3", 674 | "hdnr_blanc-c4", 675 | "hdnr_compa-c2", 676 | "hdnr_compa_v2", 677 | "hdnr_neptune", 678 | "hdnr_neptune-c2_v2", 679 | "hdnr_neptune-c3", 680 | "hdnr_neptune-c3_big_v2", 681 | "heavyarms_v2", 682 | "hell", 683 | "helltaker_modeus_v2", 684 | "helmet_black", 685 | "helmet_blue", 686 | "helmet_red", 687 | "hev_grenheart", 688 | "hevfroppy", 689 | "hevgirl_v2", 690 | "hhpay_julie", 691 | "hi3_kiana_kaslana-mb", 692 | "hi_bronya", 693 | "hibari_v2", 694 | "hikaru_v2", 695 | "hinako", 696 | "hinanai_v3", 697 | "hlgawrgura", 698 | "hlgawrgurabig", 699 | "hn_iris_heart_a_v3", 700 | "hn_iris_heart_n_v3", 701 | "hn_purple_heart_a_v2", 702 | "hn_purple_heart_n_v2", 703 | "hn_uni_maid_v2", 704 | "holo_aqua_lowpoly", 705 | "holo_baelz_lowpoly", 706 | "holo_coco_lowpoly", 707 | "holo_ina_lowpoly", 708 | "holo_irys_lowpoly", 709 | "holo_korone_lowpoly", 710 | "holo_laplus_lowpoly", 711 | "holo_mori_lowpoly", 712 | "holo_nenechi_lowpoly", 713 | "holo_okayu_lowpoly", 714 | "holo_ollie_lowpoly", 715 | "holo_pekora_lowpoly", 716 | "holo_polka_lowpoly", 717 | "holo_rushia_lowpoly", 718 | "holo_sana_lowpoly", 719 | "holo_takodachi_hsize", 720 | "holo_takodachi_psize", 721 | "holo_towa_lowpoly", 722 | "holo_watame_lowpoly", 723 | "hololive_botan", 724 | "hololive_fubuki", 725 | "hololive_gura-c1", 726 | "hololive_gura-c2", 727 | "hololive_korone-c1", 728 | "hololive_korone-c2", 729 | "hololive_marine", 730 | "hololive_marine_ld", 731 | "hololive_towa", 732 | "horo_saw", 733 | "i_inyuasha", 734 | "i_kagome", 735 | "i_kykyo", 736 | "i_miroku", 737 | "i_naraku", 738 | "i_sango", 739 | "i_sesshomaru", 740 | "ibrs", 741 | "iku", 742 | "illyasviel", 743 | "inuyasha", 744 | "itachi", 745 | "jaco", 746 | "jeice", 747 | "kagerou", 748 | "kaguyaarikawa_mazz", 749 | "kakashi_v2", 750 | "kancolle_amatsukaze", 751 | "kancolle_atago_n_v2", 752 | "kancolle_atago_v2", 753 | "kancolle_bismarck_n_v2", 754 | "kancolle_bismarck_v2", 755 | "kancolle_harbour", 756 | "kancolle_harbour2_v2", 757 | "kancolle_hibiki", 758 | "kancolle_hibiki_n", 759 | "kancolle_shimakaze", 760 | "kancolle_shimakaze_v3", 761 | "kancolle_takao_n_v3", 762 | "kancolle_takao_v2", 763 | "kanna", 764 | "kanna_christmas", 765 | "kanna_d", 766 | "kanna_halloween", 767 | "kanna_human", 768 | "kanna_urban", 769 | "karenkujo", 770 | "kasane_teto_v2", 771 | "kazextreme2_v2", 772 | "kc_akatsuki_n", 773 | "kc_akatsuki_v2", 774 | "kc_berni_n", 775 | "kc_berni_v2", 776 | "kc_destroyer_1_v2", 777 | "kc_destroyer_n", 778 | "kc_destroyer_v2", 779 | "kc_harbour_v3", 780 | "kc_harbour_v4", 781 | "kc_hibiki_v3", 782 | "kc_hoppou_tot", 783 | "kc_ikazuchi_n", 784 | "kc_ikazuchi_v2", 785 | "kc_inazuma_n", 786 | "kc_inazuma_v2", 787 | "kc_murasame_a_v2", 788 | "kc_murasame_v2", 789 | "kc_northern_avia_v2", 790 | "kc_northern_splicer_m1", 791 | "kc_northern_splicer_m2", 792 | "kc_northern_splicer_tn", 793 | "kc_northern_v3", 794 | "kc_northern_v4", 795 | "kc_shimakaze_v2_n", 796 | "keine", 797 | "kenshin", 798 | "kf_gentoo", 799 | "kf_greywolf", 800 | "kf_greywolf_kz_v3", 801 | "kf_humboldt", 802 | "kf_japwolf_v2", 803 | "kf_koutei", 804 | "kf_rockhopper", 805 | "kf_royal", 806 | "kf_sandcatv", 807 | "kf_serval", 808 | "kf_serval_v3", 809 | "kf_shoebill", 810 | "kf_shoebill_v3", 811 | "kf_tiger_v2", 812 | "kf_tsuchinoko_v2", 813 | "kikyou_v2", 814 | "kinnikuman", 815 | "kiteretu", 816 | "kiwami", 817 | "kizuna_halloween", 818 | "kizuna_winter", 819 | "kizuna_xmas", 820 | "kizuna_yin_v1", 821 | "kkaoru", 822 | "km_ayakomichi_v2", 823 | "km_karenkujo", 824 | "km_karenkujo_alt", 825 | "km_karenkujo_bel", 826 | "kmd_kanna_christmas_ld", 827 | "kmd_kanna_cop", 828 | "kmd_kanna_halloween_ld", 829 | "kmd_kanna_human_ld", 830 | "kmd_kanna_ld", 831 | "kmd_kanna_urban_ld", 832 | "kmd_kanna_v2", 833 | "kmd_kunoichi_kanna", 834 | "kmd_lucoa", 835 | "kmd_lucoa_alt", 836 | "kmd_lucoa_alt_hatm", 837 | "kmd_lucoa_bikini", 838 | "kmd_lucoa_bikini_hatm", 839 | "kmd_lucoa_hatm", 840 | "kmd_tooru_v2", 841 | "koakuma", 842 | "kof_ash", 843 | "kof_goenitz96", 844 | "kof_iori", 845 | "kof_k", 846 | "kof_kula", 847 | "kof_kyo", 848 | "kof_nakoruru", 849 | "kof_nameless", 850 | "kof_ninon", 851 | "kof_omegarugal98", 852 | "kof_orochi97", 853 | "kof_rock", 854 | "kof_terry", 855 | "kof_zero01", 856 | "koishi", 857 | "koishiscarletv1", 858 | "konata", 859 | "konata_small", 860 | "konoko", 861 | "kotori", 862 | "kurumi", 863 | "kurumi_tokisaki", 864 | "kv_aperture", 865 | "kv_zatsunemiku", 866 | "kv_zatsunemiku_s1", 867 | "kz_bowsette_v3", 868 | "kz_fgo_mheroine", 869 | "kz_lolimiku", 870 | "kz_marisa", 871 | "kz_megumi_v2", 872 | "kz_megumin", 873 | "kz_megumin_r", 874 | "kz_megumin_r_nohat", 875 | "kz_miku_w", 876 | "kz_mikutda_v4", 877 | "kz_moeglados_v2", 878 | "kz_nerutda_v2", 879 | "kz_op_evilmiku", 880 | "kz_op_uni", 881 | "kz_reimu", 882 | "kz_rindo", 883 | "kz_rindo_sw", 884 | "kz_rindo_swc_v2", 885 | "kz_rindo_v2", 886 | "kz_rindo_v3", 887 | "kz_tda_etihw", 888 | "kz_touhou_momiji_v2", 889 | "kz_yonaka", 890 | "kz_yosafire", 891 | "laura", 892 | "leon_v2", 893 | "ll_nico_yazawa", 894 | "lolinam", 895 | "lolipirate", 896 | "lolisleepy", 897 | "lt_amatsukaze", 898 | "lt_gaara", 899 | "lt_kanna_urban", 900 | "lt_naruto", 901 | "lt_ribbon", 902 | "lt_rin", 903 | "lt_sexologo_v3", 904 | "lt_shinobu", 905 | "lt_swako", 906 | "lt_thyakumo", 907 | "lulu", 908 | "luotianyi", 909 | "ma_alice_blue", 910 | "ma_alice_red", 911 | "madotuki2_v3", 912 | "madotuki_bu-n_v2", 913 | "madotuki_v3", 914 | "makimacsm", 915 | "manaka", 916 | "marisa", 917 | "marisa2", 918 | "marisa3", 919 | "maryoshi143", 920 | "matiasesf_goku", 921 | "maya_dawn_v2", 922 | "maya_dawn_v2_1", 923 | "mc_miku", 924 | "mce_ene_v2", 925 | "mdn_ankokuboshi_kurome", 926 | "mdn_tenno_uzume-c2_v2", 927 | "mdn_tenno_uzume-c3", 928 | "mdn_tenno_uzume-mizugi", 929 | "mdn_tennouboshi_uzume", 930 | "mechabrs", 931 | "mechawrs", 932 | "megatron-slaughter", 933 | "megatron_v2", 934 | "meranian_v2fa", 935 | "mercgirl", 936 | "merlin", 937 | "merunya_v1", 938 | "mha_uraraka", 939 | "mha_uraraka-visor", 940 | "mha_uraraka_s5", 941 | "mha_uraraka_school", 942 | "mhahimikoa", 943 | "mhahimikob", 944 | "miketama", 945 | "miketama_ahl", 946 | "miketama_small_v2", 947 | "mikito", 948 | "miko_b_v2", 949 | "miko_black_v2", 950 | "miko_v2", 951 | "miku", 952 | "miku2", 953 | "miku_alpha", 954 | "miku_cute2_arms", 955 | "miku_f", 956 | "miku_fabulous2_v2", 957 | "miku_kurumi_v2", 958 | "miku_neko", 959 | "miku_pierretta", 960 | "miku_verano", 961 | "mikuaction", 962 | "mikuangel", 963 | "mikuarmy", 964 | "mikuhatsune_v1", 965 | "mikuhatsune_v2", 966 | "mikuhev_v1", 967 | "mikuhev_v2", 968 | "mikunavidad", 969 | "mikuschool", 970 | "mikuskeleto", 971 | "mikuwhite", 972 | "milly_d_v2", 973 | "milly_g2", 974 | "milly_g_v2", 975 | "milly_gsg9", 976 | "milly_maid_v2", 977 | "milly_n", 978 | "milly_nurse_v2", 979 | "milly_p", 980 | "milly_p2", 981 | "milly_terror", 982 | "milly_v2", 983 | "milly_w_v2", 984 | "milly_xmas_v2", 985 | "mint", 986 | "mint_sukumizu_v2", 987 | "mio_honda_step", 988 | "mm_papi", 989 | "mm_papi_watermelon", 990 | "mmm_akemi_homura", 991 | "mokou", 992 | "momiji_inubashiri", 993 | "monoe_v2", 994 | "mg_hanekawa_0b_v2", 995 | "mghanekawa_1b_v2", 996 | "mghanekawa_b_v2", 997 | "monogatari_shinobu_v2", 998 | "monoko1_v3", 999 | "monoko2_v3", 1000 | "monspeet", 1001 | "mord_skin", 1002 | "msn-06s_sc4", 1003 | "murasame_br", 1004 | "nabeshin", 1005 | "nagato_bikini", 1006 | "nagato_nudie_v2", 1007 | "nagato_xmas", 1008 | "nagato_xmas_black", 1009 | "nagato_xmas_blackn", 1010 | "nagato_xmas_n", 1011 | "nagato_xmas_topless", 1012 | "naluri_v2", 1013 | "naruto", 1014 | "naruto_demon", 1015 | "naruto_haku", 1016 | "naruto_haku2", 1017 | "naruto_hinata", 1018 | "naruto_hinata2", 1019 | "naruto_itachi", 1020 | "naruto_kakashi", 1021 | "naruto_kisame", 1022 | "naruto_naruto", 1023 | "naruto_naruto2", 1024 | "naruto_neji", 1025 | "naruto_neji2", 1026 | "naruto_rock_lee", 1027 | "naruto_sakura", 1028 | "naruto_sasuke", 1029 | "naruto_sasuke2", 1030 | "naruto_sensei_guy", 1031 | "naruto_shikamaru", 1032 | "naruto_toon", 1033 | "naruto_zabuza", 1034 | "naruto_zabuza2", 1035 | "nataku", 1036 | "natsumi_s_v2", 1037 | "natsumi_v2", 1038 | "necoarc", 1039 | "necoarcbubbles", 1040 | "necoarcchaos", 1041 | "necoarcdestiny", 1042 | "nekoheca", 1043 | "nekomata2_disguise_v2", 1044 | "nekomata2_mappa", 1045 | "nekomata2_miko_v2", 1046 | "nekomata2_mizugi_v2", 1047 | "nekomata2_skirt2", 1048 | "nekomata2_skirt_v2", 1049 | "nekomata2_tback", 1050 | "nekomata2_xmas_v2", 1051 | "nekomata2_zinbei_mt_v2", 1052 | "nekomata2_zinbei_v2", 1053 | "nekomata_ahl2_v2", 1054 | "nekomata_ahl_v2", 1055 | "nekomata_armor_d_v2", 1056 | "nekomata_armor_v2", 1057 | "nekomata_beta_v2", 1058 | "nekomata_casual_d_v2", 1059 | "nekomata_casual_v2", 1060 | "nekomata_goth_v2", 1061 | "nekomata_jc_v2", 1062 | "nekomata_jk_v2", 1063 | "nekomata_maid_d_v2", 1064 | "nekomata_maid_v2", 1065 | "nekomata_mappa", 1066 | "nekomata_mizugi_d_v2", 1067 | "nekomata_mizugi_v2", 1068 | "nekomata_ouo", 1069 | "nekomata_scnormal_v2", 1070 | "nekomata_skirt_d_v2", 1071 | "nekomata_skirt_v2", 1072 | "nekomata_smile_v2", 1073 | "nekomata_tback", 1074 | "nekomata_zinbei_d_v2", 1075 | "nekomata_zinbei_v2", 1076 | "nekomimi_agent2_v2", 1077 | "nekomimi_agent_v2", 1078 | "nekomimi_bikini2_v2", 1079 | "nekomimi_bikini_v2", 1080 | "nekomimi_buruma_v2", 1081 | "nekomimi_casino_v2", 1082 | "nekomimi_doom_v2", 1083 | "nekomimi_fallen_v2", 1084 | "nekomimi_magical_v2", 1085 | "nekomimi_maid2_v2", 1086 | "nekomimi_maid_v2", 1087 | "nekomimi_nurse2_v2", 1088 | "nekomimi_nurse_v2", 1089 | "nekomimi_sc2_v2", 1090 | "nekomimi_sc_v2", 1091 | "nekomimi_school_v2", 1092 | "nekomimi_serious_v2", 1093 | "nekomimi_skumizu_v2", 1094 | "nekomimi_soldier_v2", 1095 | "nekomimi_space_v2", 1096 | "nekomimi_wafuku2_b_v2", 1097 | "nekomimi_wafuku2_v2", 1098 | "nekomimi_wafuku_v2", 1099 | "nekomimi_white_v2", 1100 | "nekomimi_wild", 1101 | "nekomimi_wild2", 1102 | "nekomimi_wild3", 1103 | "nekomimiku_f_v2", 1104 | "nekomimiku_v2", 1105 | "nekomimimaid_beta_v2", 1106 | "nekopara_azuki_v2", 1107 | "nekopara_chocola_v2", 1108 | "nekopara_coconut", 1109 | "nekopara_coconut_kz", 1110 | "nekopara_ethanol_red", 1111 | "nekopara_ethanol_v1", 1112 | "nekopara_ethanol_v3", 1113 | "nekopara_ethanol_v4", 1114 | "nekopara_ethanol_v5", 1115 | "nekopara_vanilla_v2", 1116 | "nekotoromaid", 1117 | "nemesis_v2", 1118 | "nepgear", 1119 | "nepgear_mazz", 1120 | "nepgear_mazzstella_1", 1121 | "nepnep2_mazz", 1122 | "nepnep3_mazz", 1123 | "nepnep_mazz", 1124 | "neptune", 1125 | "nepud_rejsyg", 1126 | "nge_asuka", 1127 | "nge_auska", 1128 | "nge_eva01", 1129 | "nge_rei", 1130 | "ngnl_jibril_v2", 1131 | "ngnl_shiro_v3", 1132 | "night_saber", 1133 | "nitori", 1134 | "nizj2_v2", 1135 | "no-face", 1136 | "nobita", 1137 | "noire_school", 1138 | "noire_v2", 1139 | "nonon_regalia", 1140 | "nr_chen", 1141 | "nr_chen_bike", 1142 | "nr_eiki", 1143 | "nr_littleanimemiku", 1144 | "nr_lolinam", 1145 | "nr_lolipirate", 1146 | "nr_lolisleepy", 1147 | "nr_okuu", 1148 | "nr_okuu_wings", 1149 | "nr_okuu_wings_folded", 1150 | "nr_ran_v2", 1151 | "nr_shinji_casual", 1152 | "nr_shinji_plugsuit", 1153 | "nr_shinji_school", 1154 | "nu_nagato_alphaq", 1155 | "nud_rejsygstella_1", 1156 | "nurani", 1157 | "nya_miku", 1158 | "nya_noire", 1159 | "nya_purple", 1160 | "nya_rabi", 1161 | "nyakotama_v2", 1162 | "nyaruko", 1163 | "onpu", 1164 | "onpu2", 1165 | "ooka_miko_2nd_v2", 1166 | "ooka_miko_v2", 1167 | "opmsaitama", 1168 | "optimus", 1169 | "optimusprime", 1170 | "ov_entoma", 1171 | "ov_entoma_noflatshade", 1172 | "owatarobo", 1173 | "owatarobo_s", 1174 | "p4_jack_frost", 1175 | "p4_jack_frost_flat", 1176 | "padoru_padoru", 1177 | "papu2017", 1178 | "patchouli", 1179 | "pclair", 1180 | "peam", 1181 | "peashy", 1182 | "pethan", 1183 | "ph_rejsygstella_2", 1184 | "philda", 1185 | "piccolo2", 1186 | "pipi_p", 1187 | "pirate_tsugumi_v2", 1188 | "plutia01_mazz_v2", 1189 | "plutia02_mazz_v2", 1190 | "plutia03_mazz_v2", 1191 | "plutia04_mazz_v2", 1192 | "plyra", 1193 | "pmarnie", 1194 | "pmarnienb", 1195 | "police1", 1196 | "police1s", 1197 | "police2", 1198 | "police2s", 1199 | "police3", 1200 | "police4", 1201 | "poniko_v3", 1202 | "pop_p", 1203 | "pqchie", 1204 | "pqyu", 1205 | "pqyukiko", 1206 | "ptepimimi", 1207 | "ptepopuko", 1208 | "purple sister", 1209 | "purple_sister", 1210 | "putico", 1211 | "putico2", 1212 | "puyoringoando", 1213 | "qtz", 1214 | "r107", 1215 | "ram", 1216 | "ram_sexy", 1217 | "ran", 1218 | "rbpixie_v2", 1219 | "recca", 1220 | "red_fox", 1221 | "red_fox2", 1222 | "redqubeley", 1223 | "rei_plush", 1224 | "rei_plushbig", 1225 | "reimu", 1226 | "reimu2", 1227 | "reimu3", 1228 | "reisen2", 1229 | "reisen3", 1230 | "reisen_v2", 1231 | "rem_bikini", 1232 | "rem_nude", 1233 | "rem_sexy", 1234 | "remilia", 1235 | "remilia2", 1236 | "rezero_emilia_mazz", 1237 | "rezero_felix", 1238 | "rezero_felix_mazz", 1239 | "rezero_felix_xmas", 1240 | "rezero_ram", 1241 | "rezero_rem", 1242 | "ribbon1", 1243 | "ribbon2", 1244 | "ribbon3", 1245 | "ribbon3.0", 1246 | "ribbon_snipe", 1247 | "ribbon_v2", 1248 | "rigell", 1249 | "riku", 1250 | "rin_black_v2", 1251 | "rinnosuke", 1252 | "rintohsaka", 1253 | "rof01", 1254 | "roshi", 1255 | "roz_suigintou_b_v2", 1256 | "roz_suigintou_p_v2", 1257 | "rumia", 1258 | "rumia2", 1259 | "rwby_blake", 1260 | "rwby_ruby", 1261 | "rwby_rubyrose", 1262 | "rwby_weiss", 1263 | "rwby_weiss_sp", 1264 | "rwby_weissschnee", 1265 | "rwby_yang", 1266 | "rx78-2_v2", 1267 | "rx78_2", 1268 | "rx93-2", 1269 | "rya", 1270 | "ryuko_matoi", 1271 | "saito", 1272 | "saito2_v2", 1273 | "sakuya", 1274 | "sam1", 1275 | "sammi_v2", 1276 | "sanae", 1277 | "sandrock", 1278 | "sangoku", 1279 | "sano_v2", 1280 | "sao-lisbeth", 1281 | "sao_sinon_v2", 1282 | "sasuke", 1283 | "sauza", 1284 | "sayin", 1285 | "sazabi_nagato_nobody", 1286 | "sazabi_v2", 1287 | "scell", 1288 | "sd_sekai_saionji", 1289 | "sengoku_nadeko", 1290 | "sesshomaru", 1291 | "setsuna", 1292 | "setsuna_ts", 1293 | "sf2_sakura", 1294 | "sg_kurisumakise", 1295 | "sg_kurisumakise_alt1", 1296 | "sg_kurisumakise_alt2", 1297 | "sg_kurisumakise_alt3", 1298 | "sg_kurisumakise_old", 1299 | "shippudden_gaara", 1300 | "shippudden_gaara2", 1301 | "shippudden_kiba", 1302 | "shippudden_naruto", 1303 | "shippudden_naruto2", 1304 | "shippudden_neji", 1305 | "shippudden_neji2", 1306 | "shippudden_sakura", 1307 | "shizuha_aki", 1308 | "smg4melony_v2", 1309 | "smol_ump9", 1310 | "snk_mikasa", 1311 | "snk_mikasa_momiji", 1312 | "snk_mikasa_origin", 1313 | "sno_ikaros", 1314 | "sns_saya_v2", 1315 | "sofi_skin", 1316 | "sofi_skinv1", 1317 | "sofiadark", 1318 | "sofiah", 1319 | "softon", 1320 | "softon_choco", 1321 | "songoku", 1322 | "songokussj", 1323 | "sp_aqua_color", 1324 | "sp_aqua_hatm_color", 1325 | "sp_aqua_hatm_v2", 1326 | "sp_aqua_v2", 1327 | "sp_arabianmiku", 1328 | "sp_arabianmiku_black", 1329 | "sp_arabianmiku_red", 1330 | "sp_arabianmiku_white", 1331 | "spess_kagerou", 1332 | "spess_kagerou_alt", 1333 | "spess_kagerou_bikini", 1334 | "spess_kagerou_ld", 1335 | "spess_kagerou2_v2", 1336 | "spess_kagerou_woof", 1337 | "spike", 1338 | "spike2_v2", 1339 | "spikespiegel", 1340 | "spilled_sakura", 1341 | "spp_mbrs", 1342 | "spp_vash", 1343 | "spp_wolf", 1344 | "ss_amakusa", 1345 | "ss_charlotte", 1346 | "ss_genjuro", 1347 | "ss_haoumaru", 1348 | "ss_rimururu", 1349 | "ss_ukyo", 1350 | "ss_wildcat", 1351 | "ss_wildcat_black", 1352 | "ss_wildcat_c", 1353 | "ssjgreyfox", 1354 | "starlight2_v2", 1355 | "strike", 1356 | "strike-a", 1357 | "suika", 1358 | "supergoku", 1359 | "suwako", 1360 | "sw_cinderace", 1361 | "sw_cinderace_c", 1362 | "sw_cinderace_r", 1363 | "takeda_shoko", 1364 | "tallgeese3", 1365 | "tarles", 1366 | "temjin", 1367 | "tenko", 1368 | "tenshi", 1369 | "tenshi2", 1370 | "tenshi3", 1371 | "tenshi_40k_sisters", 1372 | "terranrp_16_v2", 1373 | "tewi", 1374 | "tewi2", 1375 | "tgp_postlady_v2", 1376 | "th_nue_houjuu", 1377 | "the-o", 1378 | "thicc_kizuna", 1379 | "tmr_optimus_prime", 1380 | "tohkarh", 1381 | "tokisaki_kurumi", 1382 | "toradora_taiga", 1383 | "touhou_alice_v2", 1384 | "touhou_aya", 1385 | "touhou_aya2", 1386 | "touhou_aya2", 1387 | "touhou_aya3", 1388 | "touhou_cheeeeeeeeeen", 1389 | "touhou_chen", 1390 | "touhou_chen_v2", 1391 | "touhou_cirno2b", 1392 | "touhou_cirno3b", 1393 | "touhou_cirno_bikini", 1394 | "touhou_cirno_v2", 1395 | "touhou_clownpiece", 1396 | "touhou_clownpiece_v2", 1397 | "touhou_clownpiss", 1398 | "touhou_daiyousei_v2", 1399 | "touhou_flandre_bikini2", 1400 | "touhou_flandre_scarlet", 1401 | "touhou_flandre_shoes", 1402 | "touhou_flandre_socks", 1403 | "touhou_flandre_v2", 1404 | "touhou_futo", 1405 | "touhou_hieda_no_akyuu", 1406 | "touhou_hijiri", 1407 | "touhou_hina_v2", 1408 | "touhou_hina_v2", 1409 | "touhou_hinanawi", 1410 | "touhou_honmeirin", 1411 | "touhou_ikusan", 1412 | "touhou_ikusan_h", 1413 | "touhou_inaba_tewi_v2", 1414 | "touhou_sakuya_v2", 1415 | "touhou_kagerou2_v2", 1416 | "touhou_kagerou_v3", 1417 | "touhou_kaguya_v2", 1418 | "touhou_kanako", 1419 | "touhou_kazami_yuuka_1", 1420 | "touhou_kazami_yuuka_2", 1421 | "touhou_kazami_yuuka_4", 1422 | "touhou_kazami_yuuka_5", 1423 | "touhou_keine_v2", 1424 | "touhou_kirisame_marisa", 1425 | "touhou_koa_devil_v2", 1426 | "touhou_koakuma", 1427 | "touhou_k_sanae_v2", 1428 | "touhou_kogasa_v2", 1429 | "touhou_koishi", 1430 | "touhou_koishi2", 1431 | "touhou_kotohime_v2", 1432 | "touhou_letty", 1433 | "touhou_maribelhearn", 1434 | "touhou_marisa3", 1435 | "touhou_meiling2", 1436 | "touhou_meiling_v3", 1437 | "touhou_mia", 1438 | "touhou_minoriko", 1439 | "touhou_mokou", 1440 | "touhou_momiji_inuba", 1441 | "touhou_nagae_iku_v2", 1442 | "touhou_new_cirno", 1443 | "touhou_new_dai", 1444 | "touhou_new_koa", 1445 | "touhou_new_remilia", 1446 | "touhou_new_satori", 1447 | "touhou_patche", 1448 | "touhou_patche2", 1449 | "touhou_patchouli", 1450 | "touhou_ransyama", 1451 | "touhou_reimu", 1452 | "touhou_reimu3", 1453 | "touhou_reimu_mmd", 1454 | "touhou_reisen2", 1455 | "touhou_remi_dress_v2", 1456 | "touhou_remilia", 1457 | "touhou_rin_v2", 1458 | "touhou_rinnosuke", 1459 | "touhou_rumia", 1460 | "touhou_rumia_v2", 1461 | "touhou_rumia_y", 1462 | "touhou_sakuya2", 1463 | "touhou_sakuya_bikini", 1464 | "touhou_sakuya_gothic", 1465 | "touhou_sakuya_gothic2", 1466 | "touhou_sanae", 1467 | "touhou_sanae_jkc_v3", 1468 | "touhou_sanae_jkg_v3", 1469 | "touhou_sanae_jky_v3", 1470 | "touhou_sekibanki_v2", 1471 | "touhou_sekibanki_v2", 1472 | "touhou_shinmyou", 1473 | "touhou_shizuha", 1474 | "touhou_suikaabcedition", 1475 | "touhou_suwakero", 1476 | "touhou_suwako_v2", 1477 | "touhou_tenko", 1478 | "touhou_tenshi", 1479 | "touhou_tenshi_40k", 1480 | "touhou_tenshi_v4", 1481 | "touhou_udongein", 1482 | "touhou_udongein_mmd_v2", 1483 | "touhou_usamirenko", 1484 | "touhou_utsuho_v3", 1485 | "touhou_utsuho_v3", 1486 | "touhou_wakasagi", 1487 | "touhou_yagokoro_eirin", 1488 | "touhou_yakumo_ran", 1489 | "touhou_yukari_v2", 1490 | "touhou_youmu", 1491 | "touhou_youmu2what", 1492 | "touhou_youmu_su", 1493 | "touhou_youmukonpakuspe", 1494 | "touhou_yuka", 1495 | "touhou_yuyuko_fumo", 1496 | "touhou_yuyusama", 1497 | "touhou_yuyusama2", 1498 | "trunks", 1499 | "trunks_ssj", 1500 | "tsugumi_v2", 1501 | "twokinds_laura", 1502 | "uboamiku", 1503 | "uni_rejsygstella_2", 1504 | "uni_rejsygstella_3", 1505 | "uni_rejsygstella_4", 1506 | "usada", 1507 | "uzaki_hana", 1508 | "v&mgirl", 1509 | "v_mgirl", 1510 | "vanjamiev2023", 1511 | "vash", 1512 | "vash2", 1513 | "vegeta_ssj4", 1514 | "vegetaca", 1515 | "vegetafnf", 1516 | "vegetas", 1517 | "vegetassgss", 1518 | "vegetassj_blue", 1519 | "vegetassj_mystic4", 1520 | "vegita", 1521 | "vocaloid_akita", 1522 | "vocaloid_haku", 1523 | "vocaloid_kasane", 1524 | "vocaloid_miku", 1525 | "vocaloid_miku2", 1526 | "vocaloid_rin", 1527 | "vocaloid_yukari_v2", 1528 | "vocaloid_yukari_yuzuki", 1529 | "vocaloid_yuzuki-yukari", 1530 | "vocaloid_yuzuki_yukari", 1531 | "voms_pikamee_lowpoly", 1532 | "voms_tomo_lowpoly", 1533 | "vt_filian", 1534 | "vtuber_filian_sw_v2", 1535 | "vtuber_kizuna", 1536 | "vtuber_kizuna_black", 1537 | "vtuber_kizuna_ld_v3", 1538 | "wang_pengxu", 1539 | "watanabeyou", 1540 | "watanabeyou_r", 1541 | "whitequbeley", 1542 | "wingzero", 1543 | "wolfwood", 1544 | "woodpixie_v2", 1545 | "wrs_digitrevx_v3", 1546 | "wz", 1547 | "wzc", 1548 | "xalice_v2", 1549 | "xinuyasha_v2", 1550 | "xkagome_v2", 1551 | "xkikyou_v2", 1552 | "xmas_pussy", 1553 | "xsesshomaru_v2", 1554 | "yagar", 1555 | "yamcha", 1556 | "yellow_heart", 1557 | "yorha_2b_scuttle_a2", 1558 | "youmu", 1559 | "youmu_test", 1560 | "ys_haruka", 1561 | "ys_osana", 1562 | "ys_osu", 1563 | "ys_saki", 1564 | "ys_senpai", 1565 | "ys_teacher", 1566 | "ys_yandere", 1567 | "ys_yandere_insane", 1568 | "yukari", 1569 | "yukari2", 1570 | "yumi", 1571 | "yunoa_v2", 1572 | "yuuka1", 1573 | "yuuka2", 1574 | "yuuka3", 1575 | "yuuka4", 1576 | "yuuka5", 1577 | "yuukasuit", 1578 | "z_zaku", 1579 | "zabuza", 1580 | "zaku", 1581 | "zakuii", 1582 | "zbright_v2", 1583 | "zerotwo_v1", 1584 | "zerotwo_v2", 1585 | "zerotwo_v3", 1586 | "al_kawakaze", 1587 | "ark_rhmare", 1588 | "ba_asuna_bunny", 1589 | "ba_atsuko", 1590 | "ba_azusa", 1591 | "ba_azusa_ar", 1592 | "ba_hifumi", 1593 | "ba_hina", 1594 | "ba_hina_mg", 1595 | "ba_hoshino", 1596 | "ba_hoshino_sg", 1597 | "ba_iori", 1598 | "ba_izuna_bikini", 1599 | "ba_izuna_bikini_ar", 1600 | "ba_kuroko", 1601 | "ba_kuroko_ar", 1602 | "ba_mutsuki", 1603 | "ba_mutsuki_mg", 1604 | "ba_saori", 1605 | "ba_serika", 1606 | "ba_shiroko", 1607 | "ba_shiroko_ar", 1608 | "ba_wakamo", 1609 | "ba_wakamo_g", 1610 | "ba_wakamo_g_rf", 1611 | "ba_wakamo_rf", 1612 | "chibiseija", 1613 | "closers_mirai_bikini", 1614 | "cookie_diyusi", 1615 | "cookie_diyusi_no_vest", 1616 | "dr_k_v2", 1617 | "gi_barbara_dark", 1618 | "gi_ganyu_new", 1619 | "gi_ganyu_office", 1620 | "gi_hutao_dark", 1621 | "gi_keqing_dark", 1622 | "gi_kukishinobu_dark", 1623 | "gi_kukishinobu_nm_dark", 1624 | "gi_sucrose_new", 1625 | "gi_yae", 1626 | "h_hololive_marine", 1627 | "h_hololive_marine_v2", 1628 | "hecatia_lp_s", 1629 | "hime_cat", 1630 | "hk416_nc", 1631 | "hololive_fubuki_dark", 1632 | "hololive_gura-c1_black", 1633 | "hololive_gura-c1_red", 1634 | "hololive_gura-c2_black", 1635 | "hololive_gura-c2_red", 1636 | "hololive_iroha", 1637 | "ht_cerberus", 1638 | "ht_cerberus_b", 1639 | "ht_justice", 1640 | "ht_justice_alt1", 1641 | "ht_justice_altb", 1642 | "ht_justice_b", 1643 | "ht_lucifer", 1644 | "ht_lucifer_b", 1645 | "ht_modeus", 1646 | "kc_hibiki_big", 1647 | "km_karenkujo_xmas", 1648 | "koneko_chan_beta_v3", 1649 | "kz_mayacyberpunk", 1650 | "kz_mayacyberpunk_alt", 1651 | "mad_koishi_komeiji_v2", 1652 | "mad_satori_komeiji", 1653 | "mad_venti", 1654 | "meika_hime", 1655 | "meika_mikoto", 1656 | "meranian_bv1", 1657 | "mikoto_cat", 1658 | "nufyllis", 1659 | "orz_minirumia_v3", 1660 | "orz_minitewi_v1", 1661 | "shii_xmas", 1662 | "sillyfun", 1663 | "touhou_chen_v2_xmas", 1664 | "touhou_momiji_small_v2", 1665 | "veemon", 1666 | "veemon_b", 1667 | "veemon_c", 1668 | "yozora_mel", 1669 | "gfl_m4_sopmod_white", 1670 | "km_karenkujo_dark", 1671 | "yuuri_glt", 1672 | "zgundam" 1673 | ], 1674 | "vehicle": [ 1675 | "accent", 1676 | "ae86trueno", 1677 | "ae86trueno_color", 1678 | "apachef", 1679 | "apacheshit", 1680 | "barricadeautohd", 1681 | "bc_arnold", 1682 | "bc_barney", 1683 | "bc_captain", 1684 | "bc_gina", 1685 | "bc_gman", 1686 | "bc_gordon", 1687 | "bc_gus", 1688 | "bc_hwg", 1689 | "bc_ns", 1690 | "bc_otis", 1691 | "bc_recon", 1692 | "bc_scientist", 1693 | "bc_soldier", 1694 | "bc_vinnie", 1695 | "bc_vs", 1696 | "bc_wizard", 1697 | "bike_edgar", 1698 | "bike_eightball", 1699 | "bike_fatherd", 1700 | "bike_louis", 1701 | "bike_molly", 1702 | "bike_nina", 1703 | "biker_v2", 1704 | "bmrftruck", 1705 | "bmrftruck2", 1706 | "carshit1", 1707 | "carshit2", 1708 | "carshit3", 1709 | "carshit4", 1710 | "carshit5", 1711 | "citroen", 1712 | "corvet", 1713 | "csmodel", 1714 | "dc_tank", 1715 | "dc_tanks", 1716 | "f_zero_car1", 1717 | "f_zero_car2", 1718 | "f_zero_car3", 1719 | "f_zero_car4", 1720 | "fdrx7", 1721 | "fockewulftriebflugel", 1722 | "forkliftshit", 1723 | "gaz", 1724 | "gto", 1725 | "hitlerlimo", 1726 | "humvee_be", 1727 | "humvee_desert", 1728 | "humvee_jungle", 1729 | "humvee_sc", 1730 | "locust", 1731 | "mbt", 1732 | "mbts", 1733 | "nr_chen_bike", 1734 | "policecar", 1735 | "policecar2", 1736 | "saucer_v2", 1737 | "sil80", 1738 | "sprt_tiefighter", 1739 | "sprt_xwing", 1740 | "tank_mbt", 1741 | "taskforcecar", 1742 | "truck", 1743 | "vehicleshit_abrams", 1744 | "vehicleshit_submarine", 1745 | "vehicleshit_tigerii" 1746 | ] 1747 | } -------------------------------------------------------------------------------- /download.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sven Co-op Model Database 8 | 9 | 41 | 42 | 43 |

Sven Co-op Model Database

44 | 49 |
50 | 51 | 52 |
53 |

Latest models

54 | latest_models_2023_06_12.7z (9029 models, 2.4 GB download, 14.2 GB extracted) 55 | 56 |

57 | This model pack contains only the latest version of each model. This is the pack you want 58 | if you play on server with the .hipoly plugin (twilightzone). Otherwise, you may still see missing models because of players using different 59 | model names. 60 |

61 |

All models

62 | all_models_2023_06_12.7z (10739 models, 2.5 GB download, 16.7 GB extracted) 63 |

64 | This pack includes all models in this database. It should fix more missing models if you play on a 65 | server that doesn't enforce the latest "official" model names. 66 | Models that don't have version suffixes will likely be the oldest versions. 67 |

68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /flamingline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wootguy/scmodels/c964684d1f4a1c9396141ad391e61b6098aa7b3b/flamingline.gif -------------------------------------------------------------------------------- /git_init.py: -------------------------------------------------------------------------------- 1 | import os, shutil, subprocess, sys, json 2 | import requests 3 | from github import Github 4 | 5 | models_path = 'models/player/' 6 | all_dirs = [dir for dir in os.listdir(models_path) if os.path.isdir(os.path.join(models_path,dir))] 7 | all_dirs.sort() 8 | total_dirs = len(all_dirs) 9 | git_asset_root = '.git_data' 10 | username = 'wootdata' 11 | commit_user = 'wootguy' 12 | commit_email = 'w00tguy123@gmail.com' 13 | ssh_host_name = 'wootdata.github.com' # used to select an ssh key from ~/.ssh/config (this is an alias not a real host name) 14 | num_buckets = 32 15 | 16 | access_token = '' 17 | with open('/home/pi/git_access_token.txt', 'r') as file: 18 | access_token = file.read().replace('\n', '') 19 | 20 | github = Github(access_token) 21 | 22 | lowest_count = 99999 23 | 24 | def hash_string(str): 25 | hash = 0 26 | 27 | for i in range(0, len(str)): 28 | char = ord(str[i]) 29 | hash = ((hash<<5)-hash) + char 30 | hash = hash % 15485863 # prevent hash ever increasing beyond 31 bits (prime) 31 | 32 | return hash 33 | 34 | def create_repos(): 35 | print("") 36 | print("WARNING: This will delete all sc_models_* repos, locally and on GitHub.") 37 | print("The repos will then be recreated which will take a long time.") 38 | print("") 39 | 40 | input("Press Enter to continue...") 41 | 42 | if os.path.exists(git_asset_root): 43 | shutil.rmtree(git_asset_root) 44 | os.mkdir(git_asset_root) 45 | 46 | print("Initializing asset repos") 47 | for i in range(0, num_buckets): 48 | git_path = os.path.join(git_asset_root, 'repo%s' % i) 49 | 50 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'init'] 51 | subprocess.run(args) 52 | 53 | # configure commit username/email 54 | args = ['git', '--git-dir=%s' % git_path, 'config', 'user.name', '"%s"' % commit_user] 55 | subprocess.run(args) 56 | args = ['git', '--git-dir=%s' % git_path, 'config', 'user.email', '"%s"' % commit_email] 57 | subprocess.run(args) 58 | 59 | # Add files to each repo, balanced by hash key 60 | print("Adding files to repos") 61 | for idx, dir in enumerate(all_dirs): 62 | b = hash_string(dir) % num_buckets 63 | git_path = os.path.join(git_asset_root, 'repo%s' % b) 64 | 65 | print("%s -> %s" % (dir, git_path)) 66 | 67 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'add', os.path.join(models_path, dir), '-f'] 68 | subprocess.run(args) 69 | 70 | # add common files and commit in all repos 71 | for i in range(0, num_buckets): 72 | git_path = os.path.join(git_asset_root, 'repo%s' % i) 73 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'add', '.nojekyll'] 74 | subprocess.run(args) 75 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'commit', '-m', 'initial commit'] 76 | subprocess.run(args) 77 | 78 | github_user = github.get_user() 79 | 80 | # Create repos, push to them, and enable github pages 81 | for i in range(0, num_buckets): 82 | git_path = os.path.join(git_asset_root, 'repo%s' % i) 83 | repo_name = 'scmodels_data_%s' % i 84 | 85 | try: 86 | repo = github_user.get_repo(repo_name) 87 | print("Deleting existing repo: %s" % repo_name) 88 | if repo: 89 | repo.delete() 90 | except Exception as e: 91 | print(e) 92 | 93 | repo = github_user.create_repo(repo_name, description='storage partition for scmodels') 94 | print("Created %s" % repo.full_name) 95 | 96 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'remote', 'add', 'origin', 'git@%s:%s.git' % (ssh_host_name, repo.full_name)] 97 | subprocess.run(args) 98 | 99 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'push', '-u', 'origin', 'master'] 100 | subprocess.run(args) 101 | 102 | print("Enabling GitHub Pages...") 103 | headers = { 104 | 'Authorization': 'token ' + access_token, 105 | 'Accept': 'application/vnd.github.switcheroo-preview+json' 106 | } 107 | payload = { 108 | 'source': { 109 | 'branch': 'master', 110 | 'path': '' 111 | } 112 | } 113 | resp = requests.post('https://api.github.com/repos/%s/%s/pages' % (username, repo_name), headers=headers, data=json.dumps(payload)).json() 114 | print(resp) 115 | 116 | print("Repo creation finished: %s" % repo_name) 117 | print("") 118 | 119 | def update(commit_message): 120 | global all_dirs 121 | 122 | if True: 123 | # Add files to each repo, balanced by hash key 124 | print("Adding files to repos") 125 | 126 | if os.path.exists('updated.txt'): 127 | with open("updated.txt", "r") as update_list: 128 | all_dirs = update_list.readlines() 129 | print("using updated.txt instead of checking all folders") 130 | else: 131 | print("Updating all folders because updated.txt does not exist (slow!)") 132 | 133 | updated_buckets = [] 134 | 135 | for idx, dir in enumerate(all_dirs): 136 | dir = dir.strip() 137 | b = hash_string(dir) % num_buckets 138 | updated_buckets.append(b) 139 | git_path = os.path.join(git_asset_root, 'repo%s' % b) 140 | 141 | print("%s -> %s" % (dir, git_path)) 142 | 143 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'add', os.path.join(models_path, dir), '-f'] 144 | subprocess.run(args) 145 | 146 | # commit and push 147 | for i in range(0, num_buckets): 148 | if i not in updated_buckets: 149 | continue 150 | 151 | repo_name = 'scmodels_data_%s' % i 152 | print("\nUpdating %s" % repo_name) 153 | git_path = os.path.join(git_asset_root, 'repo%s' % i) 154 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'commit', '-m', commit_message] 155 | subprocess.run(args) 156 | 157 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'push'] 158 | subprocess.run(args) 159 | 160 | if os.path.exists('updated.txt'): 161 | os.remove("updated.txt") 162 | 163 | 164 | def update_simple(commit_message): 165 | global all_dirs 166 | 167 | if True: 168 | # Add files to each repo, balanced by hash key 169 | print("Adding all changes to repos") 170 | 171 | for b in range(num_buckets): 172 | git_path = os.path.join(git_asset_root, 'repo%s' % b) 173 | 174 | print("add all changes for %s" % (git_path)) 175 | 176 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'add', '-u'] 177 | subprocess.run(args) 178 | 179 | # commit and push 180 | for i in range(0, num_buckets): 181 | repo_name = 'scmodels_data_%s' % i 182 | print("\nUpdating %s" % repo_name) 183 | git_path = os.path.join(git_asset_root, 'repo%s' % i) 184 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'commit', '-m', commit_message] 185 | subprocess.run(args) 186 | 187 | args = ['git', '--git-dir=%s' % git_path, '--work-tree=.', 'push'] 188 | subprocess.run(args) 189 | 190 | 191 | args = sys.argv[1:] 192 | 193 | if len(args) == 1 and args[0].lower() == 'help' or len(args) == 0: 194 | print("\nUsage:") 195 | print("python3 git_init.py [command]\n") 196 | 197 | print("Available commands:") 198 | print("create - creates or re-creates all data repos (takes like 8 hours)") 199 | print("update 'commit message' - adds new models. Default commit message is 'add new models'") 200 | print("update_rem 'commit message' - like update, but does 'git add -u' instead of adding new individual models.") 201 | print(" This adds removed and updated files to the commit, ignoring untracked files.") 202 | 203 | print("\nIMPORTANT: DO NOT RUN THIS AS ROOT") 204 | 205 | if len(args) > 0: 206 | if args[0].lower() == 'create': 207 | create_repos() 208 | if args[0].lower() == 'update': 209 | commit_message = "add new models" 210 | if len(args) > 1: 211 | commit_message = args[1] 212 | update(commit_message) 213 | if args[0].lower() == 'update_rem': 214 | commit_message = "add new models" 215 | if len(args) > 1: 216 | commit_message = args[1] 217 | update_simple(commit_message) -------------------------------------------------------------------------------- /hlms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wootguy/scmodels/c964684d1f4a1c9396141ad391e61b6098aa7b3b/hlms -------------------------------------------------------------------------------- /hlms.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wootguy/scmodels/c964684d1f4a1c9396141ad391e61b6098aa7b3b/hlms.wasm -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sven Co-op Model Database 8 | 9 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 |

Sven Co-op Model Database

447 | 452 |
453 | 454 |
455 | 456 | 511 | 512 |
513 | 514 | 515 |
516 |
517 |
518 | 519 |
520 |
521 |
522 | 523 | 524 | 525 |
526 |

Loading (0%)

527 |
528 |
529 |

Header text

530 |
531 |
Polygons:
???
532 |
Size:
???
533 |
Animated Mouth:
???
534 |
External Models:
???
535 |
MD5:
???
536 |
Internal Name:
???
537 |
Known Aliases:
???
538 |
Sounds:
???
539 |
540 |
541 |
Download model
542 |
543 | 544 |
545 | 546 | 547 |
548 |
549 | 550 | 551 |
552 | 553 | 554 |
555 | Sequence: 556 |
557 |
558 |
559 | 560 | 561 | Last updated: June 12, 2023 562 | 563 |
564 | 565 | 618 | 619 | -------------------------------------------------------------------------------- /modeldb.js: -------------------------------------------------------------------------------- 1 | 2 | var g_model_data = {}; 3 | var g_view_model_data = {}; 4 | var g_old_versions = {}; // for filtering 5 | var g_groups = {}; 6 | var g_tags = {}; 7 | var g_verions = []; 8 | var g_aliases = {}; 9 | var g_group_filter = ''; 10 | var g_model_names; 11 | var can_load_new_model = false; 12 | var model_load_queue; 13 | var model_unload_waiting; 14 | var hlms_is_ready = false; 15 | var g_db_files_loaded = 0; 16 | var g_debug_mode = false; // for quickly creating lists of models for tagging/grouping 17 | 18 | // returning from the group view_model 19 | var g_offset_before_group = 0; 20 | var g_search_before_group = 0; 21 | 22 | var model_results; // subset of g_model_names 23 | var results_per_page = 40; 24 | //var result_offset = 0; 25 | var result_offset = 1201; 26 | var data_repo_domain = "https://wootdata.github.io/"; 27 | var g_sound_repo_url = "https://wootdata.github.io/scmodels_data_snd/"; 28 | var data_repo_count = 32; 29 | var renderWidth = 500; 30 | var renderHeight = 800; 31 | var antialias = 2; 32 | var g_3d_enabled = true; 33 | var g_model_was_loaded = false; 34 | var g_view_model_name = ""; 35 | var g_groups_with_results = {}; 36 | var g_model_path = "models/player/"; 37 | var g_downloader_interval = null; // for model downloads 38 | 39 | var g_debug_copy = ""; 40 | 41 | function fetchTextFile(path, callback) { 42 | var httpRequest = new XMLHttpRequest(); 43 | httpRequest.onreadystatechange = function() { 44 | if (httpRequest.readyState === 4 && httpRequest.status === 200 && callback) { 45 | callback(httpRequest.responseText); 46 | } 47 | }; 48 | httpRequest.open('GET', path + '?nocache=' + (new Date()).getTime()); 49 | httpRequest.send(); 50 | } 51 | 52 | function fetchBinaryFile(path, callback) { 53 | var httpRequest = new XMLHttpRequest(); 54 | httpRequest.onreadystatechange = function() { 55 | if (httpRequest.readyState === 4 && httpRequest.status === 200 && callback) { 56 | callback(httpRequest.response); 57 | } else if (httpRequest.readyState === 4 && callback) { 58 | callback(null); 59 | } 60 | }; 61 | httpRequest.open('GET', path); 62 | httpRequest.responseType = "blob"; 63 | httpRequest.send(); 64 | } 65 | 66 | function fetchJSONFile(path, callback) { 67 | fetchTextFile(path, function(data) { 68 | try { 69 | callback(JSON.parse(data)); 70 | } catch(e) { 71 | console.error("Failed to load JSON file: " + path +"\n\n", e); 72 | var loader = document.getElementsByClassName("site-loader")[0]; 73 | loader.classList.remove("loader"); 74 | loader.innerHTML = "Failed to load file: " + path + "

" + e; 75 | } 76 | }); 77 | } 78 | 79 | function stopDownloads() { 80 | if (!hlms_is_ready) { 81 | console.log("Can't cancel yet"); 82 | return; // don't want to cancel this accidentally 83 | } 84 | 85 | if (window.stop !== undefined) { 86 | window.stop(); 87 | } 88 | else if (document.execCommand !== undefined) { 89 | document.execCommand("Stop", false); 90 | } 91 | } 92 | 93 | function hlms_load_model(model_name, t_model, seq_groups) { 94 | var repo_url = get_repo_url(model_name); 95 | var model_path = repo_url + "models/player/" + model_name + "/"; 96 | 97 | if (can_load_new_model) { 98 | Module.ccall('load_new_model', null, ['string', 'string', 'string', 'number'], [model_path, model_name, t_model, seq_groups], {async: true}); 99 | can_load_new_model = false; 100 | return true; 101 | } else { 102 | console.log("Can't load a new model yet. Waiting for previous model to load."); 103 | model_load_queue = model_name; 104 | 105 | var popup = document.getElementById("model-popup"); 106 | popup.getElementsByClassName("loader")[0].style.visibility = "hidden"; 107 | popup.getElementsByClassName("loader-text")[0].textContent = "Failed to load. Try refreshing."; 108 | 109 | return false; 110 | } 111 | } 112 | 113 | function humanFileSize(size) { 114 | var i = Math.floor( Math.log(size) / Math.log(1024) ); 115 | return ( size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i]; 116 | }; 117 | 118 | function update_model_details() { 119 | var popup = document.getElementById("model-popup"); 120 | 121 | var totalPolys = 0; 122 | var hasLdModel = false; 123 | for (var i = 0; i < g_view_model_data["bodies"].length; i++) { 124 | let models = g_view_model_data["bodies"][i]["models"]; 125 | let polys = parseInt(models[0]["polys"]); 126 | 127 | if (models.length > 1) { 128 | hasLdModel = true; 129 | if (document.getElementById("cl_himodels").checked) { 130 | polys = parseInt(models[models.length-1]["polys"]); // cl_himodels 1 (default client setting) 131 | } 132 | } 133 | 134 | totalPolys += polys; 135 | } 136 | 137 | if (hasLdModel) { 138 | popup.getElementsByClassName("hd_setting")[0].style.display = "block"; 139 | } 140 | 141 | var soundTable = ""; 142 | for (var i = 0; i < g_view_model_data["events"].length; i++) { 143 | var evt = g_view_model_data["events"][i]; 144 | if (evt["event"] == 5004 && evt["options"].length > 0) { 145 | var seq = evt["sequence"]; 146 | var seqName = seq + " : " + g_view_model_data["sequences"][seq]["name"]; 147 | var path = evt["options"]; 148 | soundTable += '
' + seqName + '
' + path + "
"; 149 | } 150 | } 151 | if (soundTable.length > 0) { 152 | soundTable = '
' + soundTable + "
"; 153 | } 154 | 155 | var has_mouth = false; 156 | if (g_view_model_data["controllers"].length > 4) { 157 | var ctl = g_view_model_data["controllers"][4]; 158 | has_mouth = ctl.bone >= 0 && ctl.start != ctl.end; 159 | } 160 | 161 | var ext_mdl = "No"; 162 | var ext_tex = g_view_model_data["t_model"]; 163 | var ext_anim = g_view_model_data["seq_groups"] > 1; 164 | if (ext_tex && ext_anim) { 165 | ext_mdl = "Textures + Sequences"; 166 | } else if (ext_tex) { 167 | ext_mdl = "Textures"; 168 | } else if (ext_anim) { 169 | ext_mdl = "Sequences"; 170 | } 171 | 172 | var aliases = g_model_data[g_view_model_name]["aliases"]; 173 | if (aliases) { 174 | aliases = aliases.join("
") 175 | } 176 | 177 | popup.getElementsByClassName("polycount")[0].textContent = totalPolys.toLocaleString(undefined); 178 | popup.getElementsByClassName("polycount")[0].setAttribute("title", totalPolys.toLocaleString(undefined)); 179 | popup.getElementsByClassName("filesize")[0].textContent = humanFileSize(g_view_model_data["size"]); 180 | popup.getElementsByClassName("filesize")[0].setAttribute("title", humanFileSize(g_view_model_data["size"])); 181 | popup.getElementsByClassName("compilename")[0].textContent = g_view_model_data["name"]; 182 | popup.getElementsByClassName("compilename")[0].setAttribute("title", g_view_model_data["name"]); 183 | popup.getElementsByClassName("aliases")[0].innerHTML = aliases ? aliases : "None"; 184 | popup.getElementsByClassName("aliases")[0].setAttribute("title", aliases ? aliases.replaceAll("
", "\n") : "This model has no known aliases."); 185 | popup.getElementsByClassName("ext_mdl")[0].textContent = ext_mdl; 186 | popup.getElementsByClassName("ext_mdl")[0].setAttribute("title", ext_mdl); 187 | popup.getElementsByClassName("sounds")[0].innerHTML = soundTable.length > 0 ? soundTable : "None"; 188 | popup.getElementsByClassName("md5")[0].textContent = g_view_model_data["md5"]; 189 | popup.getElementsByClassName("md5")[0].setAttribute("title", g_view_model_data["md5"]); 190 | popup.getElementsByClassName("has_mouth")[0].textContent = has_mouth ? "Yes" : "No"; 191 | 192 | var polyColor = ""; 193 | if (totalPolys < 1000) { 194 | polyColor = "#0f0"; 195 | if (totalPolys < 600) { 196 | popup.getElementsByClassName("polycount")[0].innerHTML += "  👍"; 197 | } 198 | } else if (totalPolys < 2000) { 199 | polyColor = "white"; 200 | } else if (totalPolys < 4*1000) { 201 | polyColor = "yellow"; 202 | } else if (totalPolys < 10*1000) { 203 | polyColor = "orange"; 204 | } else { 205 | polyColor = "red"; 206 | } 207 | 208 | if (totalPolys >= 60*1000) { 209 | popup.getElementsByClassName("polyflame")[0].style.visibility = "visible"; 210 | } 211 | if (totalPolys >= 40*1000) { 212 | popup.getElementsByClassName("polycount")[0].classList.add("insane"); 213 | } 214 | if (totalPolys >= 20*1000) { 215 | popup.getElementsByClassName("polycount")[0].innerHTML = "🚨 " 216 | + popup.getElementsByClassName("polycount")[0].innerHTML + " 🚨"; 217 | } 218 | popup.getElementsByClassName("polycount")[0].style.color = polyColor; 219 | 220 | } 221 | 222 | function view_model(model_name) { 223 | var model_path = "models/player/" + model_name + "/"; 224 | var popup = document.getElementById("model-popup"); 225 | var popup_bg = document.getElementById("model-popup-bg"); 226 | var img = popup.getElementsByTagName("img")[0]; 227 | var canvas = popup.getElementsByTagName("canvas")[0]; 228 | var details = popup.getElementsByClassName("details")[0]; 229 | var repo_url = get_repo_url(model_name); 230 | popup.style.display = "block"; 231 | popup_bg.style.display = "block"; 232 | canvas.style.visibility = "hidden"; 233 | img.style.display = "block"; 234 | img.setAttribute("src", ""); 235 | img.setAttribute("src", repo_url + model_path + model_name + "_small.png"); 236 | img.setAttribute("src_large", repo_url + model_path + model_name + "_large.png"); 237 | g_view_model_name = model_name; 238 | document.getElementById("cl_himodels").checked = true; 239 | 240 | popup.getElementsByClassName("details-header")[0].textContent = model_name; 241 | popup.getElementsByClassName("polycount")[0].textContent = "???"; 242 | popup.getElementsByClassName("polycount")[0].removeAttribute("title"); 243 | popup.getElementsByClassName("filesize")[0].textContent = "???"; 244 | popup.getElementsByClassName("filesize")[0].removeAttribute("title"); 245 | popup.getElementsByClassName("compilename")[0].textContent = "???"; 246 | popup.getElementsByClassName("compilename")[0].removeAttribute("title"); 247 | popup.getElementsByClassName("ext_mdl")[0].textContent = "???"; 248 | popup.getElementsByClassName("ext_mdl")[0].removeAttribute("title"); 249 | popup.getElementsByClassName("sounds")[0].textContent = "???"; 250 | popup.getElementsByClassName("aliases")[0].textContent = "???"; 251 | popup.getElementsByClassName("aliases")[0].removeAttribute("title"); 252 | popup.getElementsByClassName("md5")[0].textContent = "???"; 253 | popup.getElementsByClassName("md5")[0].removeAttribute("title"); 254 | popup.getElementsByClassName("has_mouth")[0].textContent = "???"; 255 | popup.getElementsByClassName("loader")[0].style.visibility = "visible"; 256 | popup.getElementsByClassName("loader-text")[0].style.visibility = "visible"; 257 | popup.getElementsByClassName("loader-text")[0].textContent = "Loading (0%)"; 258 | popup.getElementsByClassName("polycount")[0].style.color = ""; 259 | popup.getElementsByClassName("polycount")[0].classList.remove("insane"); 260 | popup.getElementsByClassName("polyflame")[0].style.visibility = "hidden"; 261 | 262 | let select = popup.getElementsByClassName("animations")[0]; 263 | select.textContent = ""; 264 | 265 | canvas.style.width = "" + renderWidth + "px"; 266 | canvas.style.height = "" + renderHeight + "px"; 267 | img.style.width = "" + renderWidth + "px"; 268 | img.style.height = "" + renderHeight + "px"; 269 | details.style.height = "" + renderHeight + "px"; 270 | 271 | img.onload = function() { 272 | img.setAttribute("src", repo_url + model_path + model_name + "_large.png"); 273 | 274 | img.onload = function() { 275 | img.onload = undefined; 276 | }; 277 | } 278 | 279 | popup.getElementsByClassName("hd_setting")[0].style.display = "none"; 280 | 281 | g_model_was_loaded = false; 282 | fetchJSONFile(repo_url + model_path + model_name + ".json", function(data) { 283 | console.log(data); 284 | g_view_model_data = data; 285 | 286 | update_model_details(); 287 | 288 | if (document.getElementById("3d_on").checked) { 289 | let t_model = data["t_model"] ? model_name + "t.mdl" : ""; 290 | hlms_load_model(model_name, t_model, data["seq_groups"]); 291 | g_model_was_loaded = true; 292 | } else { 293 | popup.getElementsByClassName("loader")[0].style.visibility = "hidden"; 294 | popup.getElementsByClassName("loader-text")[0].style.visibility = "hidden"; 295 | g_model_was_loaded = false; 296 | } 297 | 298 | for (var x = 0; x < data["sequences"].length; x++ ) { 299 | let seq = document.createElement("option"); 300 | seq.textContent = "" + x + " : " + data["sequences"][x]["name"]; 301 | select.appendChild(seq); 302 | } 303 | }); 304 | } 305 | 306 | function download_model() { 307 | if (g_downloader_interval != null) { 308 | console.log("Already downloading file"); 309 | return; 310 | 311 | } 312 | var fileList = [ 313 | g_model_path + g_view_model_name + "/" + g_view_model_name + ".bmp", 314 | g_model_path + g_view_model_name + "/" + g_view_model_name + ".mdl" 315 | ]; 316 | 317 | for (var i = 0; i < g_view_model_data["seq_groups"]-1; i++) { 318 | var num = i+1; 319 | var suffix = i < 10 ? "0" + num : num; 320 | fileList.push(g_model_path + g_view_model_name + "/" + g_view_model_name + suffix + ".mdl"); 321 | } 322 | 323 | if (g_view_model_data["t_model"]) { 324 | fileList.push(g_model_path + g_view_model_name + "/" + g_view_model_name + "t.mdl"); 325 | } 326 | 327 | for (var i = 0; i < g_view_model_data["events"].length; i++) { 328 | var evt = g_view_model_data["events"][i]; 329 | if (evt["event"] == 5004 && evt["options"].length > 0) { 330 | var path = evt["options"].toLowerCase(); 331 | if (path[0] == "/" || path[0] == "\\") { 332 | path = path.substr(1); 333 | } 334 | path = "sound/" + path; 335 | 336 | if (fileList.indexOf(path) == -1) { 337 | fileList.push(path); 338 | } 339 | } 340 | } 341 | 342 | var fileData = {}; 343 | for (var i = 0; i < fileList.length; i++) { 344 | (function(path) { 345 | var repo_url = path.indexOf("sound/") == 0 ? g_sound_repo_url : get_repo_url(g_view_model_name); 346 | 347 | fetchBinaryFile(repo_url + path, function(data) { 348 | fileData[path] = data; 349 | }); 350 | })(fileList[i]); 351 | } 352 | 353 | document.getElementsByClassName("download-loader")[0].classList.remove("hidden"); 354 | 355 | clearInterval(g_downloader_interval); 356 | g_downloader_interval = setInterval(function() { 357 | if (Object.keys(fileData).length >= fileList.length) { 358 | clearInterval(g_downloader_interval); 359 | 360 | var zip = new JSZip(); 361 | 362 | for (var key in fileData) { 363 | if (fileData[key]) { 364 | zip.file(key, fileData[key]); 365 | } 366 | } 367 | 368 | document.getElementsByClassName("download-but-text")[0].textContent = "Creating Zip"; 369 | 370 | zip.generateAsync({type: "blob",compression: "DEFLATE"}).then(function(content) { 371 | document.getElementsByClassName("download-but-text")[0].textContent = "Download"; 372 | document.getElementsByClassName("download-loader")[0].classList.add("hidden"); 373 | 374 | if (g_downloader_interval == null) { // cancelled download 375 | console.log("Zip filed created but user cancelled"); 376 | return; 377 | } 378 | 379 | g_downloader_interval = null; 380 | saveAs(content, g_view_model_name + ".zip"); 381 | }); 382 | } else { 383 | document.getElementsByClassName("download-but-text")[0].innerHTML = 384 | "Downloading " + (Object.keys(fileData).length+1) + " / " + fileList.length; 385 | } 386 | }, 100); 387 | 388 | } 389 | 390 | function close_model_viewer() { 391 | var popup = document.getElementById("model-popup"); 392 | var popup_bg = document.getElementById("model-popup-bg"); 393 | popup.style.display = "none"; 394 | popup_bg.style.display = "none"; 395 | 396 | if (can_load_new_model) { 397 | Module.ccall('unload_model', null, [], [], {async: true}); 398 | } else { 399 | model_unload_waiting = true; 400 | } 401 | 402 | clearInterval(g_downloader_interval); 403 | g_downloader_interval = null; 404 | document.getElementsByClassName("download-but-text")[0].textContent = "Download model"; 405 | document.getElementsByClassName("download-loader")[0].classList.add("hidden"); 406 | } 407 | 408 | function hlms_do_queued_action() { 409 | if (model_load_queue) { 410 | if (hlms_load_model(model_load_queue)) { 411 | model_load_queue = undefined; 412 | model_unload_waiting = false; 413 | } 414 | } else if (model_unload_waiting) { 415 | Module.ccall('unload_model', null, [], [], {async: true}); 416 | model_unload_waiting = false; 417 | } 418 | } 419 | 420 | function hlms_model_load_complete(successful) { 421 | if (successful) { 422 | var popup = document.getElementById("model-popup"); 423 | var img = popup.getElementsByTagName("img")[0]; 424 | var canvas = popup.getElementsByTagName("canvas")[0]; 425 | 426 | if (document.getElementById("3d_on").checked) { 427 | canvas.style.visibility = "visible"; 428 | img.style.display = "none"; 429 | img.setAttribute("src", ""); 430 | Module.ccall('set_wireframe', null, ["number"], [document.getElementById("wireframe").checked ? 1 : 0], {async: true}); 431 | } 432 | 433 | popup.getElementsByClassName("loader")[0].style.visibility = "hidden"; 434 | popup.getElementsByClassName("loader-text")[0].style.visibility = "hidden"; 435 | 436 | console.log("Model loading finished"); 437 | } else { 438 | console.log("Model loading failed"); 439 | } 440 | 441 | can_load_new_model = true; 442 | setTimeout(function() { 443 | hlms_do_queued_action(); 444 | }, 100); 445 | } 446 | 447 | function hlms_ready() { 448 | can_load_new_model = true; 449 | hlms_is_ready = true; 450 | console.log("Model viewer is ready"); 451 | hlms_do_queued_action(); 452 | 453 | // GLFW will disable backspace and enter otherwise (WTF?) 454 | window.removeEventListener("keydown", GLFW.onKeydown, true); 455 | window.addEventListener("keydown", function() { 456 | GLFW.onKeyChanged(event.keyCode, 1); // GLFW_PRESS or GLFW_REPEAT 457 | }, true); 458 | 459 | Module.ccall('update_viewport', null, ['number', 'number'], [renderWidth*antialias, renderHeight*antialias], {async: true}); 460 | } 461 | 462 | function load_page() { 463 | stopDownloads(); 464 | 465 | document.getElementsByClassName("result-total")[0].textContent = "" + model_results.length; 466 | document.getElementsByClassName("page-start")[0].textContent = "" + (result_offset+1); 467 | document.getElementsByClassName("page-end")[0].textContent = "" + Math.min(result_offset+results_per_page, model_results.length); 468 | 469 | update_model_grid(); 470 | } 471 | 472 | function next_page() { 473 | result_offset += results_per_page; 474 | if (result_offset >= model_results.length) { 475 | result_offset -= results_per_page; 476 | return; 477 | } 478 | load_page(); 479 | } 480 | 481 | function prev_page() { 482 | result_offset -= results_per_page; 483 | if (result_offset < 0) { 484 | result_offset = 0; 485 | } 486 | load_page(); 487 | } 488 | 489 | function first_page() { 490 | result_offset = 0; 491 | load_page(); 492 | } 493 | 494 | function last_page() { 495 | result_offset = 0; 496 | while (true) { 497 | result_offset += results_per_page; 498 | if (result_offset >= model_results.length) { 499 | result_offset -= results_per_page; 500 | break; 501 | } 502 | } 503 | load_page(); 504 | } 505 | 506 | function load_results() { 507 | first_page(); 508 | } 509 | 510 | function apply_filters(no_reload) { 511 | var name_filter = document.getElementById("name-filter").value; 512 | var tag_filter = document.getElementsByClassName("categories")[0].value.toLowerCase(); 513 | var hide_old_ver = document.getElementById("filter_ver").checked; 514 | var use_groups = document.getElementById("filter_group").checked; 515 | var sort_by = document.getElementsByClassName("sort")[0].value.toLowerCase(); 516 | 517 | console.log("Applying filters"); 518 | 519 | var blacklist = {}; 520 | 521 | g_groups_with_results = {}; 522 | 523 | var name_parts = []; 524 | if (name_filter.length > 0 && Object.keys(g_model_data).length > 0) { 525 | name_parts = name_filter.toLowerCase().split(" "); 526 | } 527 | var is_tag_filtering = tag_filter.length > 0 && tag_filter != "all"; 528 | var show_group_matches = (name_parts.length > 0 || is_tag_filtering) && g_group_filter.length == 0; 529 | 530 | for (var i = 0; i < g_model_names.length; i++) { 531 | var modelName = g_model_names[i]; 532 | var group = g_model_data[modelName]["group"]; 533 | var shouldExclude = false; 534 | 535 | if (g_group_filter.length) { 536 | if (group != g_group_filter) { 537 | shouldExclude = true; 538 | } 539 | } 540 | 541 | if (!shouldExclude && is_tag_filtering) { 542 | if (!(g_model_data[modelName]["tags"]) || !g_model_data[modelName]["tags"].has(tag_filter)) { 543 | shouldExclude = true; 544 | } 545 | } 546 | 547 | if (!shouldExclude && hide_old_ver) { 548 | if (modelName in g_old_versions) { 549 | var is_group = false; 550 | for (var key in g_groups) { 551 | if (g_groups[key][0] == modelName) { 552 | is_group = true; 553 | break; 554 | } 555 | } 556 | 557 | if (!use_groups || !is_group) { 558 | shouldExclude = true; 559 | } 560 | } 561 | } 562 | 563 | if (!shouldExclude && name_parts.length > 0) { 564 | var aliases = [modelName]; 565 | if (g_model_data[modelName]["aliases"]) { 566 | aliases = aliases.concat(g_model_data[modelName]["aliases"]); 567 | } 568 | 569 | var anyMatch = false; 570 | for (var a = 0; a < aliases.length; a++) { 571 | var testName = aliases[a].toLowerCase(); 572 | 573 | var aliasMatched = true; 574 | for (var k = 0; k < name_parts.length; k++) { 575 | // TODO: Add this when it's clear that a result is shown because the group name matches: 576 | // !(group && group.toLowerCase().includes(name_parts[k])) 577 | 578 | if (!testName.includes(name_parts[k])) { 579 | aliasMatched = false; 580 | break; 581 | } 582 | } 583 | 584 | if (aliasMatched) { 585 | anyMatch = true; 586 | break; 587 | } 588 | } 589 | 590 | if (!anyMatch) { 591 | shouldExclude = true; 592 | } 593 | } 594 | 595 | if (shouldExclude) { 596 | blacklist[g_model_names[i]] = true; 597 | } 598 | else if (show_group_matches && group) { 599 | g_groups_with_results[group] = g_groups_with_results[group] ? g_groups_with_results[group] + 1 : 1; 600 | } 601 | } 602 | 603 | // remove models that are in groups, unless it's the first model or if any grouped models matched the search terms 604 | if (use_groups) { 605 | for (var key in g_groups) { 606 | if (key == g_group_filter) { 607 | continue; 608 | } 609 | for (var i = 1; i < g_groups[key].length; i++) { 610 | blacklist[g_groups[key][i]] = true; 611 | } 612 | if (g_groups_with_results[key]) { 613 | blacklist[g_groups[key][0]] = false; 614 | } 615 | } 616 | } 617 | 618 | model_results = g_model_names.filter(function (name) { 619 | return !(blacklist[name]); 620 | }); 621 | 622 | if (sort_by != "name") { 623 | if (sort_by == "polys") { 624 | model_results.sort(function(x, y) { 625 | return g_model_data[y].polys - g_model_data[x].polys; 626 | }); 627 | } else if (sort_by == "size") { 628 | model_results.sort(function(x, y) { 629 | return g_model_data[y].size - g_model_data[x].size; 630 | }); 631 | } 632 | } 633 | 634 | if (no_reload === true) { 635 | load_page(); 636 | } else { 637 | load_results(); 638 | } 639 | } 640 | 641 | var last_text = ""; 642 | 643 | function update_model_grid() { 644 | var total_models = g_model_names.length; 645 | var grid = document.getElementById("model-grid"); 646 | var cell_template = document.getElementById("model-cell-template"); 647 | var hide_old_ver = document.getElementById("filter_ver").checked && Object.keys(g_model_data).length > 0; 648 | var group_mode = document.getElementById("filter_group").checked; 649 | var is_searching = Object.keys(g_groups_with_results).length > 0; 650 | 651 | grid.innerHTML = ""; 652 | 653 | var total_cells = 0; 654 | var idx = 0; 655 | model_results.every(function(model_name) { 656 | //console.log("Loading model: " + model_name); 657 | 658 | idx += 1; 659 | if (idx <= result_offset) { 660 | return true; 661 | } 662 | 663 | let group_name = Object.keys(g_model_data).length > 0 ? g_model_data[model_name].group : undefined; 664 | let is_group = group_mode 665 | && group_name 666 | && group_name in g_groups 667 | && g_groups[group_name][0] == model_name 668 | && g_group_filter != group_name; 669 | 670 | var total_in_group = 0; 671 | if (is_group) { 672 | for (var i = 0; i < g_groups[group_name].length; i++) { 673 | var testName = g_groups[group_name][i]; 674 | var baseName = get_model_base_name(testName); 675 | if (!hide_old_ver || !g_old_versions[testName]) { 676 | total_in_group += 1; 677 | } 678 | } 679 | if (total_in_group <= 1) { 680 | is_group = false; 681 | } 682 | } 683 | 684 | var cell = cell_template.cloneNode(true); 685 | var img = cell.getElementsByTagName("img")[0]; 686 | var name = cell.getElementsByClassName("name")[0]; 687 | var repo_url = get_repo_url(model_name); 688 | cell.setAttribute("class", "model-cell"); 689 | cell.removeAttribute("id"); 690 | img.setAttribute("src", repo_url + "models/player/" + model_name + "/" + model_name + "_small.png"); 691 | 692 | if (is_group) { 693 | var group_count = cell.getElementsByClassName("model-group-count")[0]; 694 | 695 | if (is_searching) { 696 | group_count.textContent = "" + g_groups_with_results[group_name] + " / " + total_in_group + " match"; 697 | } else { 698 | group_count.textContent = "" + total_in_group + " models"; 699 | } 700 | 701 | group_count.classList.remove("hidden"); 702 | } 703 | 704 | img.addEventListener("click", function() { 705 | if (is_group) { 706 | g_group_filter = group_name; 707 | g_offset_before_group = result_offset; 708 | g_search_before_group = document.getElementById("name-filter").value; 709 | document.getElementById("group-banner").classList.remove("hidden"); 710 | document.getElementsByClassName("groupname")[0].textContent = group_name; 711 | apply_filters(); 712 | } else { 713 | view_model(model_name); 714 | } 715 | }); 716 | name.innerHTML = model_name; 717 | name.setAttribute("title", model_name); 718 | 719 | 720 | name.addEventListener("mousedown", function(event) { 721 | 722 | var oldText = event.target.textContent; 723 | if (oldText == "Copied!") { 724 | return; // don't copy the user message 725 | } 726 | 727 | event.target.textContent = oldText; 728 | 729 | // debug 730 | if (g_debug_mode) { 731 | if (g_debug_copy.length) { 732 | g_debug_copy += ',\n\t\t"' + oldText + '"'; 733 | } else { 734 | g_debug_copy += '"' + oldText + '"'; 735 | } 736 | copyStringWithNewLineToClipBoard(g_debug_copy); 737 | } 738 | else { 739 | window.getSelection().selectAllChildren(event.target); 740 | document.execCommand("copy"); 741 | } 742 | 743 | event.target.textContent = "Copied!"; 744 | 745 | setTimeout(function() { 746 | event.target.textContent = oldText; 747 | }, 800); 748 | } ); 749 | grid.appendChild(cell); 750 | 751 | total_cells += 1; 752 | return total_cells < results_per_page; 753 | }); 754 | } 755 | 756 | function copyStringWithNewLineToClipBoard(stringWithNewLines){ 757 | console.log("COPY THIS " + stringWithNewLines) 758 | // Step3: find an id element within the body to append your myFluffyTextarea there temporarily 759 | const element = document.getElementsByClassName("debug")[0]; 760 | element.innerHTML = stringWithNewLines; 761 | 762 | // Step 4: Simulate selection of your text from myFluffyTextarea programmatically 763 | element.select(); 764 | 765 | // Step 5: simulate copy command (ctrl+c) 766 | // now your string with newlines should be copied to your clipboard 767 | document.execCommand('copy'); 768 | } 769 | 770 | function get_repo_url(model_name) { 771 | var repoId = hash_code(model_name) % data_repo_count; 772 | 773 | return data_repo_domain + "scmodels_data_" + repoId + "/"; 774 | } 775 | 776 | function hash_code(str) { 777 | var hash = 0; 778 | 779 | for (var i = 0; i < str.length; i++) { 780 | var char = str.charCodeAt(i); 781 | hash = ((hash<<5)-hash)+char; 782 | hash = hash % 15485863; // prevent hash ever increasing beyond 31 bits 783 | 784 | } 785 | return hash; 786 | } 787 | 788 | function set_animation(idx) { 789 | Module.ccall('set_animation', null, ['number'], [idx], {async: true}); 790 | } 791 | 792 | function reset_zoom(idx) { 793 | Module.ccall('reset_zoom', null, [], [], {async: true}); 794 | } 795 | 796 | window.onresize = handle_resize; 797 | 798 | function handle_resize(event) { 799 | var gridWidth = document.getElementById("model-grid").offsetWidth; 800 | var pagingHeight = document.getElementsByClassName("page-num-container")[0].offsetHeight; 801 | 802 | var iconsPerRow = Math.floor( gridWidth / 145 ); 803 | var iconsPerCol = Math.floor( (window.innerHeight - pagingHeight) / 239 ); 804 | 805 | if (iconsPerCol < 1) 806 | iconsPerCol = 1; 807 | if (iconsPerRow < 1) 808 | iconsPerRow = 1; 809 | 810 | results_per_page = iconsPerRow*iconsPerCol; 811 | 812 | load_page(); 813 | 814 | renderHeight = Math.floor( Math.max(100, window.innerHeight - 100) ); 815 | renderWidth = Math.floor( renderHeight * (500.0 / 800.0) ); 816 | 817 | var maxCanvasWidth = window.innerWidth*0.4; // need some space for model details 818 | if (renderWidth > maxCanvasWidth) { 819 | renderWidth = maxCanvasWidth; 820 | renderHeight = Math.floor( renderWidth * (800.0 / 500.0) ); 821 | } 822 | 823 | var popup = document.getElementById("model-popup"); 824 | var img = popup.getElementsByTagName("img")[0]; 825 | var canvas = popup.getElementsByTagName("canvas")[0]; 826 | var details = popup.getElementsByClassName("details")[0]; 827 | 828 | if (hlms_is_ready) 829 | Module.ccall('update_viewport', null, ['number', 'number'], [renderWidth*antialias, renderHeight*antialias], {async: true}); 830 | 831 | canvas.style.width = "" + renderWidth + "px"; 832 | canvas.style.height = "" + renderHeight + "px"; 833 | img.style.width = "" + renderWidth + "px"; 834 | img.style.height = "" + renderHeight + "px"; 835 | details.style.width = "calc(100% - " + renderWidth + "px)"; 836 | details.style.height = "" + renderHeight + "px"; 837 | }; 838 | 839 | function handle_3d_toggle() { 840 | var popup = document.getElementById("model-popup"); 841 | var img = popup.getElementsByTagName("img")[0]; 842 | var canvas = popup.getElementsByTagName("canvas")[0]; 843 | 844 | if (g_3d_enabled) { 845 | canvas.style.visibility = "visible"; 846 | img.style.display = "none"; 847 | img.setAttribute("src", ""); 848 | 849 | Module.ccall('pause', null, ["number"], [0], {async: true}); 850 | if (!g_model_was_loaded) { 851 | view_model(g_view_model_name); 852 | } 853 | } else { 854 | canvas.style.visibility = "hidden"; 855 | img.style.display = "block"; 856 | img.setAttribute("src", img.getAttribute("src_large")); 857 | 858 | Module.ccall('pause', null, ["number"], [1], {async: true}); 859 | } 860 | } 861 | 862 | function get_model_base_name(name) { 863 | var ver_regex = /_v\d+$/g; 864 | var verSuffix = name.match(ver_regex); 865 | 866 | if (verSuffix) { 867 | return name.replace(verSuffix[0], ""); 868 | } 869 | 870 | return name; 871 | } 872 | 873 | function json_post_load() { 874 | console.log("JSON POST LOAD"); 875 | document.getElementsByClassName("content")[0].classList.remove("hidden"); 876 | document.getElementsByClassName("site-loader")[0].classList.add("hidden"); 877 | 878 | if (g_debug_mode) { 879 | document.getElementsByClassName("debug")[0].classList.remove("hidden"); 880 | } 881 | 882 | var initialGroupData = JSON.parse(JSON.stringify(g_groups)); 883 | 884 | // process group info 885 | for (var key in g_groups) { 886 | for (var i = 0; i < g_groups[key].length; i++) { 887 | var name = g_groups[key][i]; 888 | if (!(name in g_model_data)) { 889 | console.error("MISSING MODEL: " + name + " in group " + key); 890 | continue; 891 | } 892 | 893 | if (g_model_data[name]["group"]) { 894 | if (g_model_data[name]["group"] != key) { 895 | console.error(name + " is in group '" + g_model_data[name]["group"] + "' AND '" + key + "'"); 896 | } else { 897 | console.error(name + " is in group '" + g_model_data[name]["group"] + "' more than once"); 898 | } 899 | } 900 | g_model_data[name]["group"] = key; 901 | } 902 | } 903 | 904 | // process tag info 905 | var categories = document.getElementsByClassName("categories")[0]; 906 | for (var tag in g_tags) { 907 | let opt = document.createElement("option"); 908 | opt.textContent = tag.charAt(0).toUpperCase() + tag.slice(1); 909 | categories.appendChild(opt); 910 | 911 | for (var i = 0; i < g_tags[tag].length; i++) { 912 | var model = g_tags[tag][i]; 913 | 914 | if (!(model in g_model_data)) { 915 | console.error("tags.json model does not exist: " + model); 916 | continue; 917 | } 918 | 919 | if (!("tags" in g_model_data[model])) { 920 | g_model_data[model]["tags"] = new Set(); 921 | } 922 | 923 | g_model_data[model]["tags"].add(tag); 924 | } 925 | } 926 | 927 | // process version info 928 | var all_old_versions = new Set(); 929 | for (var i = 0; i < g_versions.length; i++) { 930 | // skip first value of the list, which is the latest version 931 | var latest_version = g_versions[i][0]; 932 | if (!(latest_version in g_model_data)) { 933 | console.error("versions.json model not found: " + latest_version); 934 | continue; 935 | } 936 | 937 | for (var k = 1; k < g_versions[i].length; k++) { 938 | var modelName = g_versions[i][k]; 939 | if (!(modelName in g_model_data)) { 940 | console.error("versions.json model not found: " + modelName); 941 | continue; 942 | } 943 | 944 | all_old_versions.add(modelName); 945 | g_old_versions[modelName] = true; 946 | var parentGroup = g_model_data[latest_version]["group"]; 947 | 948 | // inherit group/tag info from latest version 949 | if (parentGroup) { 950 | g_groups[parentGroup].push(modelName); 951 | g_model_data[modelName]["group"] = g_model_data[latest_version]["group"]; 952 | } else { 953 | var newGroupName = get_model_base_name(modelName); 954 | g_groups[newGroupName] = [latest_version, modelName]; 955 | g_model_data[modelName]["group"] = g_model_data[latest_version]["group"] = newGroupName; 956 | } 957 | 958 | if (g_model_data[latest_version]["tags"]) { 959 | g_model_data[modelName]["tags"] = new Set(g_model_data[latest_version]["tags"]); 960 | } 961 | } 962 | } 963 | 964 | // process alias info 965 | for (var key in g_aliases) { 966 | if (!g_model_data[key]) { 967 | console.error("Aliases for unknown model: " + key); 968 | continue; 969 | } 970 | g_model_data[key]["aliases"] = g_aliases[key]; 971 | } 972 | 973 | // make sure the sort keys exist 974 | for (var key in g_model_data) { 975 | g_model_data[key]['polys'] = g_model_data[key]['polys'] || -1; 976 | g_model_data[key]['size'] = g_model_data[key]['size'] || -1; 977 | } 978 | 979 | // check that the latest versions are used in groups/tags 980 | for (var key in initialGroupData) { 981 | for (var i = 0; i < initialGroupData[key].length; i++) { 982 | var model = initialGroupData[key][i]; 983 | 984 | if (all_old_versions.has(model)) { 985 | console.error("Old model version in group " + key + ": " + model); 986 | } 987 | } 988 | } 989 | for (var key in g_tags) { 990 | for (var i = 0; i < g_tags[key].length; i++) { 991 | var model = g_tags[key][i]; 992 | 993 | if (all_old_versions.has(model)) { 994 | console.error("Old model version in tags.json: " + model); 995 | } 996 | } 997 | } 998 | 999 | apply_filters(); 1000 | handle_resize(); 1001 | } 1002 | 1003 | function wait_for_json_to_load() { 1004 | if (g_db_files_loaded < 6) { 1005 | setTimeout(function() { 1006 | wait_for_json_to_load(); 1007 | }, 10); 1008 | } else { 1009 | console.log("All json files loaded. Time to process them."); 1010 | json_post_load(); 1011 | } 1012 | } 1013 | 1014 | function load_database_files() { 1015 | fetchTextFile("database/model_names.txt", function(data) { 1016 | g_model_names = data.split("\n"); 1017 | g_model_names = g_model_names.filter(function (name) { 1018 | return name.length > 0; 1019 | }); 1020 | 1021 | console.log("loaded " + g_model_names.length + " model names"); 1022 | 1023 | g_model_names.sort(function(x, y) { 1024 | if (x.toLowerCase() < y.toLowerCase()) { 1025 | return -1; 1026 | } 1027 | return 1; 1028 | }); 1029 | 1030 | 1031 | //model_results = g_model_names; 1032 | //apply_filters(); 1033 | //handle_resize(); 1034 | 1035 | g_db_files_loaded += 1; 1036 | }); 1037 | 1038 | fetchJSONFile("database/models.json", function(data) { 1039 | console.log("Global model data: ", data); 1040 | g_model_data = data; 1041 | g_db_files_loaded += 1; 1042 | }); 1043 | 1044 | fetchJSONFile("database/versions.json", function(versions) { 1045 | console.log("Version info: ", versions); 1046 | g_versions = versions; 1047 | g_db_files_loaded += 1; 1048 | }); 1049 | 1050 | fetchJSONFile("database/tags.json", function(tags) { 1051 | console.log("Tag info: ", tags); 1052 | g_tags = tags; 1053 | g_db_files_loaded += 1; 1054 | }); 1055 | 1056 | fetchJSONFile("database/groups.json", function(data) { 1057 | console.log("Group data (from server): ", data); 1058 | g_groups = data; 1059 | g_db_files_loaded += 1; 1060 | }); 1061 | 1062 | fetchJSONFile("database/alias.json", function(data) { 1063 | console.log("Aliases: ", data); 1064 | g_aliases = data; 1065 | g_db_files_loaded += 1; 1066 | }); 1067 | 1068 | wait_for_json_to_load(); 1069 | } 1070 | 1071 | document.addEventListener("DOMContentLoaded",function() { 1072 | load_database_files(); 1073 | 1074 | document.getElementById("model-popup-bg").addEventListener("click", close_model_viewer); 1075 | document.getElementsByClassName("page-next-container")[0].addEventListener("click", next_page); 1076 | document.getElementsByClassName("page-prev-container")[0].addEventListener("click", prev_page); 1077 | document.getElementsByClassName("page-first-container")[0].addEventListener("click", first_page); 1078 | document.getElementsByClassName("page-last-container")[0].addEventListener("click", last_page); 1079 | document.getElementsByClassName("download-but")[0].addEventListener("click", download_model); 1080 | document.getElementById("name-filter").addEventListener("keyup", apply_filters); 1081 | document.getElementsByClassName('categories')[0].onchange = function() { 1082 | apply_filters(); 1083 | }; 1084 | document.getElementsByClassName('sort')[0].onchange = function() { 1085 | apply_filters(); 1086 | } 1087 | document.getElementsByClassName('animations')[0].onchange = function() { 1088 | set_animation(this.selectedIndex); 1089 | }; 1090 | document.getElementById("3d_on").onchange = function() { 1091 | g_3d_enabled = this.checked; 1092 | handle_3d_toggle(); 1093 | }; 1094 | document.getElementById("cl_himodels").onchange = function() { 1095 | let body = this.checked ? 255 : 0; 1096 | Module.ccall('set_body', null, ["number"], [body], {async: true}); 1097 | update_model_details(); 1098 | }; 1099 | document.getElementById("wireframe").onchange = function() { 1100 | Module.ccall('set_wireframe', null, ["number"], [this.checked ? 1 : 0], {async: true}); 1101 | }; 1102 | document.getElementById("filter_ver").onchange = function() { 1103 | var use_groups = document.getElementById("filter_group").checked; 1104 | apply_filters(use_groups && g_group_filter.length == 0); 1105 | }; 1106 | document.getElementById("filter_group").onchange = function() { 1107 | apply_filters(); 1108 | }; 1109 | document.getElementsByClassName("group-back")[0].addEventListener("click", function() { 1110 | g_group_filter = ""; 1111 | document.getElementById("group-banner").classList.add("hidden"); 1112 | document.getElementById("name-filter").value = g_search_before_group; 1113 | apply_filters(); 1114 | 1115 | result_offset = g_offset_before_group; 1116 | load_page(); 1117 | }); 1118 | 1119 | if (g_debug_mode) { 1120 | document.onkeypress = function (e) { 1121 | e = e || window.event; 1122 | g_debug_copy = ""; 1123 | console.log("CLEARED DEBUG COPY"); 1124 | }; 1125 | } 1126 | 1127 | }); 1128 | -------------------------------------------------------------------------------- /modelguy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wootguy/scmodels/c964684d1f4a1c9396141ad391e61b6098aa7b3b/modelguy -------------------------------------------------------------------------------- /scmodels.py: -------------------------------------------------------------------------------- 1 | # sudo apt install libglew-dev libosmesa-dev pngcrush 2 | 3 | import sys, os, shutil, collections, json, subprocess, stat, hashlib, traceback, time 4 | from datetime import datetime 5 | from glob import glob 6 | from io import StringIO 7 | 8 | # TODO: 9 | # - some models I added _v2 to are actually a completely different model 10 | # - delete all thumbs.db and .ztmp 11 | # - add to alias when renaming 12 | 13 | master_json = {} 14 | master_json_name = 'database/models.json' 15 | hash_json_name = 'database/hashes.json' 16 | replacements_json_name = 'database/replacements.json' 17 | alias_json_name = 'database/alias.json' 18 | versions_json_name = 'database/versions.json' 19 | tags_json_name = 'database/tags.json' 20 | groups_json_name = 'database/groups.json' 21 | 22 | start_dir = os.getcwd() 23 | 24 | models_path = 'models/player/' 25 | install_path = 'install/' 26 | hlms_path = os.path.join(start_dir, 'hlms') 27 | modelguy_path = os.path.join(start_dir, 'modelguy') 28 | posterizer_path = '/home/pi/mediancut-posterizer/posterize' 29 | pngcrush_path = 'pngcrush' 30 | magick_path = 'convert' 31 | debug_render = False 32 | 33 | FL_CRASH_MODEL = 1 # model that crashes the game or model viewer 34 | 35 | 36 | # assumes chdir'd to the model directory beforehand 37 | def fix_case_sensitivity_problems(model_dir, expected_model_path, expected_bmp_path, work_path): 38 | global start_dir 39 | global models_path 40 | 41 | all_files = [file for file in os.listdir('.') if os.path.isfile(file)] 42 | icase_model = '' 43 | icase_preview = '' 44 | for file in all_files: 45 | if (file.lower() == expected_model_path.lower()): 46 | icase_model = file 47 | if (file.lower() == expected_bmp_path.lower()): 48 | icase_preview = file 49 | 50 | icase_model_original = icase_model 51 | icase_preview_original = icase_preview 52 | icase_model = os.path.splitext(icase_model)[0] 53 | icase_preview = os.path.splitext(icase_preview)[0] 54 | 55 | if (icase_model and icase_model != model_dir) or \ 56 | (icase_preview and icase_preview != model_dir) or \ 57 | (icase_model and icase_preview and icase_model != icase_preview): 58 | print("\nFound case-sensitive differences:\n") 59 | print("DIR (1): " + model_dir) 60 | print("MDL (2): " + icase_model) 61 | print("BMP (3): " + icase_preview) 62 | while True: 63 | x = input("\nWhich capitalization should be used? (enter 1, 2, or 3) ") 64 | 65 | correct_name = model_dir 66 | if x == '1': 67 | correct_name = model_dir 68 | elif x == '2': 69 | correct_name = icase_model 70 | elif x == '3': 71 | correct_name = icase_preview 72 | else: 73 | continue 74 | 75 | rename_model(model_dir, correct_name, work_path) 76 | 77 | return correct_name 78 | return model_dir 79 | 80 | def get_sorted_dirs(path): 81 | all_dirs = [dir for dir in os.listdir(path) if os.path.isdir(os.path.join(path,dir))] 82 | return sorted(all_dirs, key=str.casefold) 83 | 84 | def get_model_modified_date(mdl_name, work_path): 85 | mdl_path = os.path.join(work_path, mdl_name, mdl_name + ".mdl") 86 | return int(os.path.getmtime(mdl_path)) 87 | 88 | def rename_model(old_dir_name, new_name, work_path): 89 | global master_json 90 | global master_json_name 91 | global start_dir 92 | 93 | os.chdir(start_dir) 94 | 95 | old_dir = os.path.join(work_path, old_dir_name) 96 | new_dir = os.path.join(work_path, new_name) 97 | if not os.path.isdir(old_dir): 98 | print("Can't rename '%s' because that dir doesn't exist" % old_dir) 99 | return False 100 | 101 | if (old_dir_name != new_name and os.path.exists(new_dir)): 102 | print("Can't rename folder to %s. That already exists." % new_dir) 103 | return False 104 | 105 | if old_dir != new_dir: 106 | os.rename(old_dir, new_dir) 107 | print("Renamed %s -> %s" % (old_dir, new_dir)) 108 | os.chdir(new_dir) 109 | 110 | all_files = [file for file in os.listdir('.') if os.path.isfile(file)] 111 | mdl_files = [] 112 | tmdl_files = [] 113 | bmp_files = [] 114 | png_files = [] 115 | json_files = [] 116 | for file in all_files: 117 | if ".mdl" in file.lower(): 118 | mdl_files.append(file) 119 | #if ".mdl" in file.lower() and (file == old_dir_name + "t.mdl" or file == old_dir_name + "T.mdl"): 120 | # tmdl_files.append(file) 121 | if ".bmp" in file.lower(): 122 | bmp_files.append(file) 123 | if '_large.png' in file.lower() or '_small.png' in file.lower() or '_tiny.png' in file.lower(): 124 | png_files.append(file) 125 | if ".json" in file.lower(): 126 | json_files.append(file) 127 | 128 | if len(mdl_files) > 1: 129 | print("Multiple mdl files to rename. Don't know what to do") 130 | sys.exit() 131 | return False 132 | if len(tmdl_files) > 1: 133 | print("Multiple T mdl files to rename. Don't know what to do") 134 | sys.exit() 135 | return False 136 | if len(bmp_files) > 1: 137 | print("Multiple bmp files to rename. Don't know what to do") 138 | sys.exit() 139 | return False 140 | if len(json_files) > 1: 141 | print("Multiple json files to rename. Don't know what to do") 142 | sys.exit() 143 | return False 144 | if len(png_files) > 3: 145 | print("Too many PNG files found. Don't know what to do") 146 | sys.exit() 147 | return False 148 | 149 | def rename_file(file_list, new_name, ext): 150 | if len(file_list) > 0: 151 | old_file_name = file_list[0] 152 | new_file_name = new_name + ext 153 | 154 | if old_file_name != new_file_name: 155 | os.rename(old_file_name, new_file_name) 156 | print("Renamed %s -> %s" % (old_file_name, new_file_name)) 157 | 158 | rename_file(bmp_files, new_name, '.bmp') 159 | rename_file(mdl_files, new_name, '.mdl') 160 | rename_file(tmdl_files, new_name, 't.mdl') 161 | rename_file(json_files, new_name, '.json') 162 | 163 | for png_file in png_files: 164 | old_file_name = png_file 165 | 166 | new_file_name = '' 167 | if '_large' in old_file_name: 168 | new_file_name = new_name + "_large.png" 169 | elif '_small' in old_file_name: 170 | new_file_name = new_name + "_small.png" 171 | elif '_tiny' in old_file_name: 172 | new_file_name = new_name + "_tiny.png" 173 | 174 | if old_file_name != new_file_name: 175 | os.rename(old_file_name, new_file_name) 176 | print("Renamed %s -> %s" % (old_file_name, new_file_name)) 177 | 178 | return True 179 | 180 | def handle_renamed_model(model_dir, work_path): 181 | all_files = [file for file in os.listdir('.') if os.path.isfile(file)] 182 | model_files = [] 183 | for file in all_files: 184 | if '.mdl' in file.lower(): 185 | model_files.append(file) 186 | while len(model_files) >= 1: 187 | print("\nThe model file(s) in this folder do not match the folder name:\n") 188 | print("0) " + model_dir) 189 | for idx, file in enumerate(model_files): 190 | print("%s) %s" % (idx+1, file)) 191 | print("r) Enter a new name") 192 | print("d) Delete this model") 193 | x = input("\nWhich model should be used? ") 194 | 195 | if x == 'd': 196 | os.chdir(start_dir) 197 | shutil.rmtree(os.path.join(work_path, model_dir)) 198 | return '' 199 | elif x == '0': 200 | if (not rename_model(model_dir, model_dir, work_path)): 201 | continue 202 | return model_dir 203 | elif x == 'r': 204 | x = input("What should the model name be? ") 205 | if (not rename_model(model_dir, x, work_path)): 206 | continue 207 | return x 208 | elif x.isnumeric(): 209 | x = int(x) - 1 210 | if x < 0 or x >= len(model_files): 211 | continue 212 | correct_name = os.path.splitext(model_files[idx-1])[0] 213 | if (not rename_model(model_dir, correct_name, work_path)): 214 | continue 215 | return correct_name 216 | else: 217 | continue 218 | return model_dir 219 | else: 220 | while True: 221 | x = input("\nNo models exist in this folder! Delete it? (y/n) ") 222 | if x == 'y': 223 | os.chdir(start_dir) 224 | shutil.rmtree(os.path.join(work_path, model_dir)) 225 | break 226 | if x == 'n': 227 | break 228 | return model_dir 229 | 230 | def get_lowest_polycount(): 231 | global hlms_path 232 | global models_path 233 | global start_dir 234 | 235 | all_dirs = get_sorted_dirs(models_path) 236 | total_dirs = len(all_dirs) 237 | 238 | lowest_count = 99999 239 | 240 | for idx, dir in enumerate(all_dirs): 241 | model_name = dir 242 | json_path = model_name + ".json" 243 | 244 | os.chdir(start_dir) 245 | os.chdir(os.path.join(models_path, dir)) 246 | 247 | if os.path.exists(json_path): 248 | with open(json_path) as f: 249 | json_dat = f.read() 250 | dat = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 251 | tri_count = int(dat['tri_count']) 252 | if tri_count < 300 and tri_count >= 0: 253 | #print("%s = %s" % (model_name, tri_count)) 254 | print(model_name) 255 | 256 | def check_for_broken_models(): 257 | global hlms_path 258 | global models_path 259 | global start_dir 260 | 261 | all_dirs = get_sorted_dirs(models_path) 262 | total_dirs = len(all_dirs) 263 | 264 | for idx, dir in enumerate(all_dirs): 265 | model_name = dir 266 | mdl_path = model_name + ".mdl" 267 | 268 | os.chdir(start_dir) 269 | os.chdir(os.path.join(models_path, dir)) 270 | 271 | if os.path.isfile(mdl_path): 272 | try: 273 | args = [hlms_path, './' + mdl_path] 274 | output = subprocess.check_output(args) 275 | except Exception as e: 276 | output = e 277 | print(e) 278 | print("Bad model: %s" % model_name) 279 | else: 280 | print("Missing model: %s" % model_name) 281 | 282 | def generate_info_json(model_name, mdl_path, output_path): 283 | data = {} 284 | output = '' 285 | 286 | if os.path.exists(output_path): 287 | os.remove(output_path) 288 | 289 | try: 290 | args = [modelguy_path, 'info', mdl_path, output_path] 291 | output = subprocess.check_output(args) 292 | except Exception as e: 293 | output = e 294 | print(e) 295 | 296 | def update_models(work_path, skip_existing=True, skip_on_error=False, errors_only=True, info_only=False, update_master_json=False): 297 | global master_json 298 | global master_json_name 299 | global hash_json_name 300 | global magick_path 301 | global pngcrush_path 302 | global posterizer_path 303 | global hlms_path 304 | global start_dir 305 | 306 | all_dirs = get_sorted_dirs(work_path) 307 | total_dirs = len(all_dirs) 308 | 309 | list_file = None 310 | if update_master_json: 311 | list_file = open("database/model_names.txt","w") 312 | failed_models = [] 313 | longname_models = [] 314 | 315 | hash_json = {} 316 | 317 | for idx, dir in enumerate(all_dirs): 318 | model_name = dir 319 | print("IDX: %s / %s: %s " % (idx, total_dirs-1, model_name), end='\r') 320 | 321 | #garg.mdl build/asdf 1000x1600 0 1 1 322 | 323 | if len(dir) > 22: 324 | longname_models.append(dir) 325 | 326 | os.chdir(start_dir) 327 | os.chdir(os.path.join(work_path, dir)) 328 | 329 | mdl_path = model_name + ".mdl" 330 | bmp_path = model_name + ".bmp" 331 | 332 | if not os.path.isfile(mdl_path) or not os.path.isfile(bmp_path): 333 | if not skip_on_error: 334 | model_name = dir = fix_case_sensitivity_problems(dir, mdl_path, bmp_path, work_path) 335 | mdl_path = model_name + ".mdl" 336 | bmp_path = model_name + ".bmp" 337 | 338 | if not os.path.isfile(mdl_path): 339 | model_name = dir = handle_renamed_model(dir, work_path) 340 | mdl_path = model_name + ".mdl" 341 | bmp_path = model_name + ".bmp" 342 | if not os.path.isfile(mdl_path): 343 | continue 344 | 345 | if errors_only: 346 | continue 347 | 348 | mdl_path = model_name + ".mdl" 349 | bmp_path = model_name + ".bmp" 350 | render_path = model_name + "000.png" 351 | sequence = "0" 352 | frames = "1" 353 | loops = "1" 354 | 355 | info_json_path = model_name + ".json" 356 | tiny_thumb = model_name + "_tiny.png" 357 | small_thumb = model_name + "_small.png" 358 | large_thumb = model_name + "_large.png" 359 | 360 | thumbnails_generated = os.path.isfile(tiny_thumb) and os.path.isfile(small_thumb) and os.path.isfile(large_thumb) 361 | 362 | anything_updated = False 363 | broken_model = False 364 | 365 | try: 366 | if (not os.path.isfile(info_json_path) or not skip_existing): 367 | print("\nGenerating info json...") 368 | anything_updated = True 369 | generate_info_json(model_name, mdl_path, info_json_path) 370 | else: 371 | pass #print("Info json already generated") 372 | 373 | if ((not thumbnails_generated or not skip_existing) and not info_only): 374 | print("\nRendering hi-rez image...") 375 | anything_updated = True 376 | 377 | with open(os.devnull, 'w') as devnull: 378 | args = [hlms_path, mdl_path, model_name, "1000x1600", sequence, frames, loops] 379 | null_stdout=None if debug_render else devnull 380 | subprocess.check_call(args, stdout=null_stdout) 381 | 382 | def create_thumbnail(name, size, posterize_colors): 383 | print("Creating %s thumbnail..." % name) 384 | temp_path = "./%s_%s_temp.png" % (model_name, name) 385 | final_path = "./%s_%s.png" % (model_name, name) 386 | subprocess.check_call([magick_path, "./" + render_path, "-resize", size, temp_path], stdout=null_stdout) 387 | subprocess.check_call([posterizer_path, posterize_colors, temp_path, final_path], stdout=null_stdout) 388 | subprocess.check_call([pngcrush_path, "-ow", "-s", final_path], stdout=null_stdout) 389 | os.remove(temp_path) 390 | 391 | create_thumbnail("large", "500x800", "255") 392 | create_thumbnail("small", "125x200", "16") 393 | create_thumbnail("tiny", "20x32", "8") 394 | 395 | os.remove(render_path) 396 | else: 397 | pass #print("Thumbnails already generated") 398 | except Exception as e: 399 | print(e) 400 | traceback.print_exc() 401 | failed_models.append(model_name) 402 | broken_model = True 403 | anything_updated = False 404 | if not skip_on_error: 405 | sys.exit() 406 | 407 | if update_master_json: 408 | list_file.write("%s\n" % model_name) 409 | 410 | if update_master_json: 411 | filter_dat = {} 412 | 413 | if os.path.isfile(info_json_path): 414 | with open(info_json_path) as f: 415 | json_dat = f.read() 416 | infoJson = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 417 | 418 | totalPolys = 0 419 | totalPolysLd = 0 420 | hasLdModel = False 421 | for body in infoJson["bodies"]: 422 | models = body["models"] 423 | polys = int(models[0]["polys"]) 424 | 425 | if len(models) > 1: 426 | hasLdModel = True 427 | totalPolysLd += polys 428 | polys = int(models[len(models)-1]["polys"]) 429 | totalPolys += polys 430 | else: 431 | totalPolys += polys 432 | 433 | filter_dat['polys'] = totalPolys 434 | #filter_dat['polys_ld'] = totalPolysLd 435 | filter_dat['size'] = infoJson["size"] 436 | 437 | flags = 0 438 | if broken_model: 439 | flags |= FL_CRASH_MODEL 440 | filter_dat['flags'] = flags 441 | 442 | hash = infoJson['md5'] 443 | if hash not in hash_json: 444 | hash_json[hash] = [model_name] 445 | else: 446 | hash_json[hash].append(model_name) 447 | 448 | master_json[model_name] = filter_dat 449 | 450 | os.chdir(start_dir) 451 | 452 | if update_master_json: 453 | with open(master_json_name, 'w') as outfile: 454 | json.dump(master_json, outfile) 455 | with open(hash_json_name, 'w') as outfile: 456 | json.dump(hash_json, outfile) 457 | list_file.close() 458 | 459 | print("\nFinished!") 460 | 461 | if len(failed_models): 462 | print("\nFailed to update these models:") 463 | for fail in failed_models: 464 | print(fail) 465 | 466 | if len(longname_models): 467 | print("\nThe following models have names longer than 22 characters and should be renamed:") 468 | for fail in longname_models: 469 | print(fail) 470 | 471 | def write_updated_models_list(): 472 | global models_path 473 | global master_json_name 474 | 475 | oldJson = {} 476 | if os.path.exists(master_json_name): 477 | with open(master_json_name) as f: 478 | json_dat = f.read() 479 | oldJson = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 480 | 481 | all_dirs = get_sorted_dirs(models_path) 482 | 483 | list_file = open("updated.txt","w") 484 | 485 | for idx, dir in enumerate(all_dirs): 486 | if dir not in oldJson: 487 | list_file.write("%s\n" % dir) 488 | 489 | list_file.close() 490 | 491 | def validate_model_isolated(): 492 | 493 | boxId = 1 # TODO: unique id per request 494 | fileSizeQuota = '--fsize=8192' # max written/modified file size in KB 495 | processMax = '--processes=1' 496 | maxTime = '--time=60' 497 | modelName = 'white.mdl' 498 | 499 | print("Cleaning up") 500 | try: 501 | args = ['isolate', '--box-id=%d' % boxId, '--cleanup'] 502 | output = subprocess.check_output(args) 503 | except Exception as e: 504 | print(e) 505 | print(output) 506 | 507 | print("Initializing isolate") 508 | output = '' 509 | try: 510 | args = ['isolate', '--box-id=%d' % boxId, '--init'] 511 | print(' '.join(args)) 512 | output = subprocess.check_output(args) 513 | except Exception as e: 514 | print(e) 515 | print(output) 516 | return False 517 | 518 | output = output.decode('utf-8').replace("\n", '') 519 | 520 | boxPath = os.path.join(output, "box") 521 | hlmsPath = os.path.join(boxPath, "hlms") 522 | print("Isolate path: %s" % boxPath) 523 | 524 | print("Copying files") 525 | shutil.copyfile(modelName, os.path.join(boxPath, modelName)) 526 | shutil.copyfile('hlms', os.path.join(boxPath, 'hlms')) 527 | os.chmod(os.path.join(boxPath, 'hlms'), stat.S_IRWXU) 528 | 529 | success = False 530 | 531 | print("Running hlms") 532 | output = '' 533 | try: 534 | 535 | args = ['isolate', fileSizeQuota, processMax, maxTime, '--box-id=%d' % boxId, '--run', '--', './hlms', modelName, 'asdf', '16x16', '0', '1', '1'] 536 | print(' '.join(args)) 537 | output = subprocess.check_output(args) 538 | except Exception as e: 539 | print(e) 540 | print(output) 541 | success = False 542 | 543 | print("Cleaning up") 544 | try: 545 | args = ['isolate', '--box-id=%d' % boxId, '--cleanup'] 546 | output = subprocess.check_output(args) 547 | except Exception as e: 548 | print(e) 549 | print(output) 550 | 551 | return success 552 | 553 | def create_list_file(): 554 | global hlms_path 555 | global models_path 556 | global start_dir 557 | 558 | all_dirs = get_sorted_dirs(models_path) 559 | total_dirs = len(all_dirs) 560 | 561 | lower_dirs = [dir.lower() for dir in all_dirs] 562 | 563 | list_file = open("models.txt","w") 564 | min_replace_polys = 143 # set this to the default LD poly count ("player-10up") 565 | 566 | for idx, dir in enumerate(all_dirs): 567 | model_name = dir 568 | json_path = model_name + ".json" 569 | 570 | os.chdir(start_dir) 571 | os.chdir(os.path.join(models_path, dir)) 572 | 573 | if (idx % 100 == 0): 574 | print("Progress: %d / %d" % (idx, len(all_dirs))) 575 | 576 | if os.path.exists(json_path): 577 | with open(json_path) as f: 578 | json_dat = f.read() 579 | dat = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 580 | tri_count = int(dat['tri_count']) 581 | replace_model = '' # blank = use default LD model 582 | if '2d_' + model_name.lower() in lower_dirs: 583 | replace_model = '2d_' + model_name 584 | if tri_count < min_replace_polys: 585 | replace_model = model_name 586 | 587 | list_file.write("%s / %d / %s / %s\n" % (model_name.lower(), tri_count, '', replace_model.lower())) 588 | 589 | list_file.close() 590 | 591 | def hash_md5(model_file, t_model_file): 592 | hash_md5 = hashlib.md5() 593 | with open(model_file, "rb") as f: 594 | for chunk in iter(lambda: f.read(4096), b""): 595 | hash_md5.update(chunk) 596 | if t_model_file: 597 | with open(t_model_file, "rb") as f: 598 | for chunk in iter(lambda: f.read(4096), b""): 599 | hash_md5.update(chunk) 600 | return hash_md5.hexdigest() 601 | 602 | def load_all_model_hashes(path): 603 | global start_dir 604 | 605 | print("Loading model hashes in path: %s" % path) 606 | 607 | all_dirs = get_sorted_dirs(path) 608 | total_dirs = len(all_dirs) 609 | 610 | model_hashes = {} 611 | 612 | for idx, dir in enumerate(all_dirs): 613 | model_name = dir 614 | json_path = model_name + ".json" 615 | 616 | os.chdir(start_dir) 617 | os.chdir(os.path.join(path, dir)) 618 | 619 | if (idx % 100 == 0): 620 | print("Progress: %d / %d" % (idx, len(all_dirs)), end="\r") 621 | 622 | if os.path.exists(json_path): 623 | with open(json_path) as f: 624 | json_dat = f.read() 625 | dat = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 626 | if 'md5' not in dat: 627 | os.remove(json_path) 628 | print("\nMissing hash for %s. Deleted the json for it." % json_path) 629 | continue 630 | hash = dat['md5'] 631 | 632 | if hash not in model_hashes: 633 | model_hashes[hash] = [model_name] 634 | else: 635 | model_hashes[hash].append(model_name) 636 | else: 637 | print("\nMissing info JSON for %s" % model_name) 638 | 639 | print("Progress: %d / %d" % (len(all_dirs), len(all_dirs))) 640 | os.chdir(start_dir) 641 | 642 | return model_hashes 643 | 644 | # it takes a long time to load model hashes for thousands of models, so the list of hashes is saved 645 | # in a single file whenever the database is updated. This loads much faster. 646 | def load_cached_model_hashes(): 647 | global hash_json_name 648 | 649 | with open(hash_json_name) as f: 650 | json_dat = f.read() 651 | return json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 652 | 653 | return None 654 | 655 | 656 | def find_duplicate_models(work_path): 657 | model_hashes = load_all_model_hashes(work_path) 658 | print("\nAll duplicates:") 659 | 660 | for hash in model_hashes: 661 | if len(model_hashes[hash]) > 1: 662 | print("%s" % model_hashes[hash]) 663 | 664 | to_delete = [] 665 | 666 | for hash in model_hashes: 667 | if len(model_hashes[hash]) > 1: 668 | print("") 669 | for idx, model in enumerate(model_hashes[hash]): 670 | print("%d) %s" % (idx, model)) 671 | keepIdx = int(input("Which model to keep (pick a number)?")) 672 | 673 | for idx, model in enumerate(model_hashes[hash]): 674 | if idx == keepIdx: 675 | continue 676 | to_delete.append(model) 677 | 678 | ''' 679 | print("\nDuplicates with %s prefix:" % prefix) 680 | prefix = "bio_" 681 | for hash in model_hashes: 682 | if len(model_hashes[hash]) > 1: 683 | total_rem = 0 684 | for model in model_hashes[hash]: 685 | if model.lower().startswith(prefix): 686 | to_delete.append(model) 687 | total_rem += 1 688 | 689 | if total_rem == len(model_hashes[hash]): 690 | print("WOW HOW THAT HAPPEN %s" % model_hashes[hash]) 691 | input("Press enter if this is ok") 692 | ''' 693 | 694 | ''' 695 | print("\nDuplicates with the same names:") 696 | for hash in model_hashes: 697 | if len(model_hashes[hash]) > 1: 698 | same_names = True 699 | first_name = model_hashes[hash][0].lower() 700 | for name in model_hashes[hash]: 701 | if name.lower() != first_name: 702 | same_names = False 703 | break 704 | if not same_names: 705 | continue 706 | 707 | print("%s" % model_hashes[hash]) 708 | to_delete += model_hashes[hash][1:] 709 | ''' 710 | 711 | all_dirs = get_sorted_dirs(work_path) 712 | all_dirs_lower = [dir.lower() for dir in all_dirs] 713 | unique_dirs_lower = sorted(list(set(all_dirs_lower))) 714 | 715 | for ldir in unique_dirs_lower: 716 | matches = [] 717 | for idx, dir2 in enumerate(all_dirs_lower): 718 | if dir2 == ldir: 719 | matches.append(all_dirs[idx]) 720 | if len(matches) > 1: 721 | msg = ', '.join(["%s (%s)" % (dir, get_model_modified_date(dir, work_path)) for dir in matches]) 722 | print("Conflicting model names: %s" % msg) 723 | 724 | if (len(to_delete) == 0): 725 | print("\nNo duplicates to remove") 726 | return False 727 | 728 | print("\nMarked for deletion:") 729 | for dir in to_delete: 730 | print(dir) 731 | 732 | input("Press enter to delete the above %s models" % len(to_delete)) 733 | 734 | os.chdir(start_dir) 735 | for dir in to_delete: 736 | shutil.rmtree(os.path.join(work_path, dir)) 737 | 738 | return True 739 | 740 | def get_latest_version_name(model_name, versions_json): 741 | for vergroup in versions_json: 742 | for veridx in range(0, len(vergroup)): 743 | if vergroup[veridx] == model_name: 744 | return vergroup[0] 745 | 746 | return model_name 747 | 748 | def fix_json(): 749 | global versions_json_name 750 | global tags_json_name 751 | global groups_json_name 752 | global replacements_json_name 753 | 754 | versions_json = None 755 | tags_json = None 756 | groups_json = None 757 | replacements_json = None 758 | 759 | with open(versions_json_name) as f: 760 | versions_json = json.loads(f.read(), object_pairs_hook=collections.OrderedDict) 761 | with open(tags_json_name) as f: 762 | tags_json = json.loads(f.read(), object_pairs_hook=collections.OrderedDict) 763 | with open(groups_json_name) as f: 764 | groups_json = json.loads(f.read(), object_pairs_hook=collections.OrderedDict) 765 | 766 | num_updates = 0 767 | 768 | if os.path.exists(replacements_json_name): 769 | print("-- Checking replacements") 770 | new_replacement_json = {} 771 | with open(replacements_json_name) as f: 772 | replacements_json = json.loads(f.read(), object_pairs_hook=collections.OrderedDict) 773 | for key, replacements in replacements_json.items(): 774 | print("%s " % (key), end='\r') 775 | 776 | for idx in range(0, len(replacements)): 777 | latest_name = get_latest_version_name(replacements[idx], versions_json) 778 | if latest_name != replacements[idx]: 779 | print("%s -> %s " % (replacements[idx], latest_name)) 780 | replacements[idx] = latest_name 781 | num_updates += 1 782 | 783 | latest_name = get_latest_version_name(key, versions_json) 784 | if latest_name != key: 785 | new_replacement_json[latest_name] = replacements 786 | print("%s -> %s " % (key, latest_name)) 787 | num_updates += 1 788 | else: 789 | new_replacement_json[key] = replacements 790 | replacements_json = new_replacement_json 791 | 792 | print("-- Checking tags") 793 | for key, group in tags_json.items(): 794 | print("%s " % (key), end='\r') 795 | 796 | for idx in range(0, len(group)): 797 | latest_name = get_latest_version_name(group[idx], versions_json) 798 | 799 | if latest_name != group[idx]: 800 | print("%s -> %s " % (group[idx], latest_name)) 801 | group[idx] = latest_name 802 | num_updates += 1 803 | 804 | tags_json[key] = sorted(tags_json[key]) 805 | 806 | print("\n-- Checking groups") 807 | for key, group in groups_json.items(): 808 | print("%s " % (key), end='\r') 809 | 810 | for idx in range(0, len(group)): 811 | latest_name = get_latest_version_name(group[idx], versions_json) 812 | 813 | if latest_name != group[idx]: 814 | print("%s -> %s " % (group[idx], latest_name)) 815 | group[idx] = latest_name 816 | num_updates += 1 817 | 818 | # don't sort so that most appropraite model can be placed as group thumbnail 819 | #groups_json[key] = sorted(groups_json[key]) 820 | 821 | with open(tags_json_name, 'w') as outfile: 822 | tags_json = dict(sorted(tags_json.items())) 823 | json.dump(tags_json, outfile, indent=4) 824 | print("Wrote %s " % tags_json_name) 825 | 826 | with open(groups_json_name, 'w') as outfile: 827 | groups_json = dict(sorted(groups_json.items())) 828 | json.dump(groups_json, outfile, indent=4) 829 | print("Wrote %s " % groups_json_name) 830 | 831 | with open(replacements_json_name, 'w') as outfile: 832 | groups_json = dict(sorted(groups_json.items())) 833 | json.dump(replacements_json, outfile, indent=4) 834 | print("Wrote %s " % replacements_json_name) 835 | 836 | print("\nUpdated %d model references to the latest version" % num_updates) 837 | 838 | def install_new_models(new_versions_mode=False): 839 | global models_path 840 | global install_path 841 | global alias_json_name 842 | global versions_json_name 843 | global start_dir 844 | 845 | new_dirs = get_sorted_dirs(install_path) 846 | if len(new_dirs) == 0: 847 | print("No models found in %s" % install_path) 848 | sys.exit() 849 | 850 | alt_names = {} 851 | if os.path.exists(alias_json_name): 852 | with open(alias_json_name) as f: 853 | json_dat = f.read() 854 | alt_names = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 855 | 856 | # First generate info jsons, if needed 857 | print("-- Generating info JSONs for new models") 858 | update_models(install_path, True, True, False, True, False) 859 | 860 | print("\n-- Checking for duplicates") 861 | 862 | any_dups = False 863 | install_hashes = load_all_model_hashes(install_path) 864 | 865 | for hash in install_hashes: 866 | if len(install_hashes[hash]) > 1: 867 | msg = '' 868 | for model in install_hashes[hash]: 869 | msg += ' ' + model 870 | print("ERROR: Duplicate models in install folder:" + msg) 871 | any_dups = True 872 | 873 | model_hashes = load_cached_model_hashes() 874 | dups = [] 875 | 876 | for hash in install_hashes: 877 | if hash in model_hashes: 878 | print("ERROR: %s is a duplicate of %s" % (install_hashes[hash], model_hashes[hash])) 879 | dups += install_hashes[hash] 880 | any_dups = True 881 | 882 | primary_name = model_hashes[hash][0].lower() 883 | for alt in install_hashes[hash]: 884 | alt = alt.lower() 885 | if alt == primary_name: 886 | continue 887 | if primary_name not in alt_names: 888 | alt_names[primary_name] = [] 889 | if alt not in alt_names[primary_name]: 890 | alt_names[primary_name].append(alt) 891 | 892 | with open(alias_json_name, 'w') as outfile: 893 | json.dump(alt_names, outfile, indent=4) 894 | 895 | if len(dups) > 0 and input("\nDelete the duplicate models in the install folder? (y/n)") == 'y': 896 | for dup in dups: 897 | path = os.path.join(install_path, dup) 898 | shutil.rmtree(path) 899 | new_dirs = get_sorted_dirs(install_path) 900 | 901 | old_dirs = [dir for dir in os.listdir(models_path) if os.path.isdir(os.path.join(models_path,dir))] 902 | old_dirs_lower = [dir.lower() for dir in old_dirs] 903 | 904 | alt_name_risk = False 905 | 906 | for dir in new_dirs: 907 | lowernew = dir.lower() 908 | is_unique_name = True 909 | 910 | for idx, old in enumerate(old_dirs): 911 | if lowernew == old.lower(): 912 | if new_versions_mode: 913 | is_unique_name = False 914 | else: 915 | print("ERROR: %s already exists" % old) 916 | any_dups = True 917 | #rename_model(old, old + "_v2", models_path) 918 | 919 | if is_unique_name and new_versions_mode: 920 | any_dups = True 921 | print("ERROR: %s is not an update to any model. No model with that name exists." % dir) 922 | 923 | if not new_versions_mode: 924 | # not checking alias in new version mode because the models will be renamed 925 | # altough technically there can be an alias problem still, but that should be really rare 926 | for key, val in alt_names.items(): 927 | for alt in val: 928 | if alt.lower() == lowernew: 929 | print("WARNING: %s is a known alias of %s" % (lowernew, key)) 930 | alt_name_risk = True 931 | 932 | if any_dups: 933 | if new_versions_mode: 934 | print("No models were added because some models have no known older version.") 935 | else: 936 | print("No models were added due to duplicates.") 937 | return 938 | 939 | too_long_model_names = False 940 | for dir in new_dirs: 941 | if len(dir) > 22: 942 | too_long_model_names = True 943 | print("Model name too long: %s" % dir) 944 | 945 | if too_long_model_names: 946 | # the game refuses to load models with long names, and servers refuse to transfer them to clients 947 | print("No models were added due to invalid model names.") 948 | return 949 | 950 | if alt_name_risk: 951 | x = input("\nContinue adding models even though people probably have different versions of these installed? (y/n): ") 952 | if x != 'y': 953 | return 954 | 955 | print("\n-- Lowercasing files") 956 | for dir in new_dirs: 957 | all_files = [file for file in os.listdir(os.path.join(install_path, dir))] 958 | mdl_files = [] 959 | for file in all_files: 960 | if file != file.lower(): 961 | src = os.path.join(install_path, dir, file) 962 | dst = os.path.join(install_path, dir, file.lower()) 963 | if os.path.exists(dst): 964 | print("Lowercase file already exists: %s" % dst) 965 | sys.exit() 966 | else: 967 | print("Rename: %s -> %s" % (file, file.lower())) 968 | os.rename(src, dst) 969 | if dir != dir.lower(): 970 | print("Rename: %s -> %s" % (dir, dir.lower())) 971 | os.rename(os.path.join(install_path, dir), os.path.join(install_path, dir.lower())) 972 | new_dirs = [dir.lower() for dir in new_dirs] 973 | 974 | if new_versions_mode: 975 | print("\n-- Adding version suffixes") 976 | renames = [] 977 | 978 | versions_json = None 979 | with open(versions_json_name) as f: 980 | json_dat = f.read() 981 | versions_json = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 982 | 983 | for dir in new_dirs: 984 | found_ver = '' 985 | version_list_size = 1 986 | group_idx = -1 987 | for groupidx in range(0, len(versions_json)): 988 | group = versions_json[groupidx] 989 | for idx in range(0, len(group)): 990 | if group[idx] == dir: 991 | found_ver = group[0] 992 | version_list_size = len(group) 993 | group_idx = groupidx 994 | break 995 | if found_ver: 996 | break 997 | 998 | new_name = dir + "_v2" 999 | if found_ver: 1000 | fidx = found_ver.rfind("_v") 1001 | if fidx == -1 or not found_ver[fidx+2:].isnumeric(): 1002 | print("ERROR: Failed to find version number in %s. Don't know what to do." % new_name) 1003 | sys.exit() 1004 | vernum = int(found_ver[fidx+2:]) 1005 | while True: 1006 | new_name = found_ver[:fidx] + ("_v%d" % (vernum+1)) 1007 | # TODO: this assumes all files in the database are lowercase, but shouldn't maybe 1008 | if new_name.lower() not in old_dirs: 1009 | break 1010 | vernum += 1 1011 | else: 1012 | fidx = dir.rfind("_v") 1013 | if fidx != -1 and dir[fidx+2:].isnumeric(): 1014 | vernum = int(dir[fidx+2]) 1015 | while True: 1016 | new_name = dir[:fidx] + ("_v%d" % (vernum+1)) 1017 | # TODO: this assumes all files in the database are lowercase, but shouldn't maybe 1018 | if new_name.lower() not in old_dirs: 1019 | break 1020 | vernum += 1 1021 | 1022 | old_dirs.append(new_name.lower()) 1023 | 1024 | print("INFO: %s will be renamed to %s" % (dir, new_name)) 1025 | renames.append((dir, new_name.lower())) 1026 | 1027 | if group_idx != -1: 1028 | versions_json[group_idx] = [new_name] + versions_json[group_idx] 1029 | print(" %s" % versions_json[group_idx]) 1030 | else: 1031 | versions_json.append([new_name.lower(), dir.lower()]) 1032 | #print(" %s" % versions_json[-1]) 1033 | 1034 | 1035 | 1036 | print("\nWARNING: Proceeding will rename the above models and overwrite versions.json!") 1037 | print(" If this process fails you will need to undo everything manually (versions.json + model names).") 1038 | x = input("Proceed? (y/n): ") 1039 | if x != 'y': 1040 | return 1041 | 1042 | for rename_op in renames: 1043 | rename_model(rename_op[0], rename_op[1], install_path) 1044 | 1045 | os.chdir(start_dir) 1046 | 1047 | with open(versions_json_name, 'w') as outfile: 1048 | json.dump(versions_json, outfile, indent=4) 1049 | print("Wrote %s" % versions_json_name) 1050 | 1051 | print() 1052 | print("Restarting add process with new model names...") 1053 | print() 1054 | install_new_models(False) 1055 | return 1056 | 1057 | # TODO: auto-update groups and tags to use latest version names 1058 | 1059 | print("\n-- Generating thumbnails") 1060 | update_models(install_path, True, False, False, False, False) 1061 | 1062 | print("\n-- Adding %s new models" % len(new_dirs)) 1063 | for dir in new_dirs: 1064 | src = os.path.join(install_path, dir) 1065 | dst = os.path.join(models_path, dir) 1066 | shutil.move(src, dst) 1067 | 1068 | print("\n-- Updating model list and master json") 1069 | write_updated_models_list() 1070 | update_models(models_path, True, True, False, False, True) 1071 | 1072 | print("\nFinished adding models. Next:") 1073 | print("- python3 git_init.py update") 1074 | print("- Update alias.json if you renamed any models") 1075 | print("- Update groups.json and tags.json if needed") 1076 | print("- Update replacements.json in TooManyPlugins plugin if any new") 1077 | print("- Change the last-updated date in index.html") 1078 | print("- git add -A; git commit; git push;") 1079 | print("") 1080 | print("If any model sounds were added:") 1081 | print("- git --git-dir=.git_snd --work-tree=. add sound -f") 1082 | print("- commit and push") 1083 | print("") 1084 | 1085 | def pack_models(all_models): 1086 | global models_path 1087 | 1088 | crash_models = set() 1089 | with open("database/crash_models.txt", "r") as update_list: 1090 | for line in update_list.readlines(): 1091 | crash_models.add(line.lower().strip()) 1092 | 1093 | if all_models: 1094 | fname = 'all_models_%s.zip' % datetime.today().strftime('%Y-%m-%d') 1095 | cmd = 'zip -r %s models/player -x "*.png" -x "*.json"' % fname 1096 | print(cmd) 1097 | os.system(cmd) 1098 | # TODO: remove crash models from archive 1099 | return 1100 | 1101 | add_models = [] 1102 | with open(versions_json_name) as f: 1103 | json_dat = f.read() 1104 | versions = json.loads(json_dat, object_pairs_hook=collections.OrderedDict) 1105 | 1106 | exclude = crash_models 1107 | for group in versions: 1108 | for idx, name in enumerate(group): 1109 | if idx == 0: 1110 | continue 1111 | exclude.add(name.lower()) 1112 | 1113 | old_dirs = get_sorted_dirs(models_path) 1114 | all_dirs = [dir for dir in os.listdir(models_path) if os.path.isdir(os.path.join(models_path,dir)) and dir.lower() not in exclude] 1115 | all_dirs = sorted(all_dirs, key=str.casefold) 1116 | 1117 | list_file = open("zip_latest.txt","w") 1118 | for dir in all_dirs: 1119 | for file in os.listdir(os.path.join(models_path, dir)): 1120 | if file.endswith('.mdl') or file.endswith('.bmp'): 1121 | file_line = os.path.join(models_path, dir, file).replace('[', '\\[').replace(']', '\\]') 1122 | list_file.write("%s\n" % file_line) 1123 | list_file.close() 1124 | 1125 | fname = 'models/latest_models_%s.zip' % datetime.today().strftime('%Y-%m-%d') 1126 | cmd = 'zip %s -r . -i@zip_latest.txt' % fname 1127 | print(cmd) 1128 | os.system(cmd) 1129 | os.remove("zip_latest.txt") 1130 | 1131 | print("\nFinished!") 1132 | 1133 | 1134 | args = sys.argv[1:] 1135 | 1136 | if len(args) == 0 or (len(args) == 1 and args[0].lower() == 'help'): 1137 | print("\nUsage:") 1138 | print("python3 scmodels.py [command]\n") 1139 | 1140 | print("Available commands:") 1141 | print("update - generate thumbnails and info jsons for any new models, and updates model lists.") 1142 | print("regen - regenerates info jsons for every model") 1143 | print("regen_full - regenerates info jsons AND thumbnails for all models (will take hours)") 1144 | print("rename - rename model to ") 1145 | print("rename_fast - skips the update so you can rename multiple models quickly.") 1146 | print(" but you have to remember to run update afterwards.") 1147 | print("list - creates a txt file which lists every model and its poly count") 1148 | print("dup - find duplicate files (people sometimes rename models)") 1149 | print("add - add new models from the install folder") 1150 | print("add_version - add new models from the install folder, but treat them as updates to models that already exist") 1151 | print(" This will add/edit version suffixes and update versions.json.") 1152 | print("fix_json - Makes sure tags.json and groups.json are using the latest model names, and sorts jsons.") 1153 | print(" Run this after add_version.") 1154 | print("pack [latest] - pack all models into a zip file (default), or only the latest versions") 1155 | 1156 | sys.exit() 1157 | 1158 | if len(args) > 0: 1159 | if args[0].lower() == 'add': 1160 | # For adding new models 1161 | install_new_models(new_versions_mode=False) 1162 | elif args[0].lower() == 'add_version': 1163 | # For adding new models 1164 | install_new_models(new_versions_mode=True) 1165 | elif args[0].lower() == 'fix_json': 1166 | # For adding new models 1167 | fix_json() 1168 | elif args[0].lower() == 'update': 1169 | # For adding new models 1170 | update_models(models_path, skip_existing=True, skip_on_error=True, errors_only=False, info_only=False, update_master_json=True) 1171 | elif args[0].lower() == 'regen': 1172 | update_models(models_path,skip_existing=False, skip_on_error=True, errors_only=False, info_only=True, update_master_json=True) 1173 | elif args[0].lower() == 'regen_full': 1174 | update_models(models_path,skip_existing=False, skip_on_error=True, errors_only=False, info_only=False, update_master_json=True) 1175 | elif args[0].lower() == 'list': 1176 | create_list_file() 1177 | elif args[0].lower() == 'dup': 1178 | find_duplicate_models(models_path) 1179 | elif args[0].lower() == 'dup_install': 1180 | find_duplicate_models(install_path) 1181 | elif args[0].lower() == 'validate': 1182 | validate_model_isolated() 1183 | elif args[0].lower() == 'pack': 1184 | all_models = True 1185 | if len(args) > 1 and args[1].lower() == "latest": 1186 | all_models = False 1187 | 1188 | pack_models(all_models) 1189 | elif args[0].lower() == 'rename': 1190 | print("TODO: Add to alias after rename") 1191 | rename_model(args[1], args[2], models_path) 1192 | os.chdir(start_dir) 1193 | update_models(models_path, skip_existing=True, skip_on_error=True, errors_only=False, info_only=True, update_master_json=True) 1194 | list_file = open("updated.txt","w") 1195 | list_file.write("%s\n" % args[1]) 1196 | list_file.write("%s\n" % args[2]) 1197 | list_file.close() 1198 | print("\nFinished rename. Next:") 1199 | print("- update name in groups.json (TODO: automate)") 1200 | print("- update name in versions.json") 1201 | print("- python3 git_init.py update") 1202 | print("- push changes to main repo") 1203 | elif args[0].lower() == 'rename_fast': 1204 | print("TODO: Add to alias after rename") 1205 | rename_model(args[1], args[2], models_path) 1206 | os.chdir(start_dir) 1207 | list_file = open("updated.txt","a") 1208 | list_file.write("%s\n" % args[1]) 1209 | list_file.write("%s\n" % args[2]) 1210 | list_file.close() 1211 | elif args[0].lower() == 'fixup': 1212 | pass 1213 | 1214 | ''' 1215 | new_dirs = get_sorted_dirs(install_path) 1216 | 1217 | old_dirs = [dir for dir in os.listdir(models_path) if os.path.isdir(os.path.join(models_path,dir))] 1218 | old_dirs_lower = [dir.lower() for dir in old_dirs] 1219 | 1220 | for dir in new_dirs: 1221 | #if not os.path.exists(os.path.join(models_path, dir + '_v2')): 1222 | # continue 1223 | if os.path.exists(os.path.join(models_path, dir + '_v2')): 1224 | continue 1225 | 1226 | lowernew = dir.lower() 1227 | for idx, old in enumerate(old_dirs): 1228 | if lowernew == old.lower(): 1229 | newDate = get_model_modified_date(dir, install_path) 1230 | oldDate = get_model_modified_date(dir, models_path) 1231 | 1232 | diff = "NEWER" if oldDate > newDate else "OLDER" 1233 | 1234 | if oldDate < newDate: 1235 | #print("RENAME " + dir) 1236 | #rename_model(dir, dir + "_v2", install_path) 1237 | #os.chdir(start_dir) 1238 | #break 1239 | pass 1240 | 1241 | print("ERROR: %s already exists (%s)" % (old, diff)) 1242 | any_dups = True 1243 | #rename_model(old, old + "_v2", models_path) 1244 | ''' 1245 | else: 1246 | print("Unrecognized command. Run without options to see help") 1247 | 1248 | #update_models(skip_existing=True, errors_only=False) 1249 | 1250 | #check_for_broken_models() 1251 | #get_lowest_polycount() --------------------------------------------------------------------------------