├── .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 |
457 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 | 1 - X of Y
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 | Back
502 | Viewing group: ???
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
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()
--------------------------------------------------------------------------------