797927d069eeb4a6a6cd7b62f12547bc7a96636a
[xonotic/netradiant.git] / radiant / entity.cpp
1 /*
2    Copyright (C) 1999-2006 Id Software, Inc. and contributors.
3    For a list of contributors, see the accompanying CONTRIBUTORS file.
4
5    This file is part of GtkRadiant.
6
7    GtkRadiant is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    GtkRadiant is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with GtkRadiant; if not, write to the Free Software
19    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20  */
21
22 #include "entity.h"
23
24 #include "ientity.h"
25 #include "iselection.h"
26 #include "imodel.h"
27 #include "ifilesystem.h"
28 #include "iundo.h"
29 #include "editable.h"
30
31 #include "eclasslib.h"
32 #include "scenelib.h"
33 #include "os/path.h"
34 #include "os/file.h"
35 #include "stream/stringstream.h"
36 #include "stringio.h"
37
38 #include "gtkutil/filechooser.h"
39 #include "gtkmisc.h"
40 #include "select.h"
41 #include "map.h"
42 #include "preferences.h"
43 #include "gtkdlgs.h"
44 #include "mainframe.h"
45 #include "qe3.h"
46 #include "commands.h"
47
48 #include "uilib/uilib.h"
49
50 struct entity_globals_t {
51     Vector3 color_entity;
52
53     entity_globals_t() :
54             color_entity(0.0f, 0.0f, 0.0f)
55     {
56     }
57 };
58
59 entity_globals_t g_entity_globals;
60
61 class EntitySetKeyValueSelected : public scene::Graph::Walker {
62     const char *m_key;
63     const char *m_value;
64 public:
65     EntitySetKeyValueSelected(const char *key, const char *value)
66             : m_key(key), m_value(value)
67     {
68     }
69
70     bool pre(const scene::Path &path, scene::Instance &instance) const
71     {
72         return true;
73     }
74
75     void post(const scene::Path &path, scene::Instance &instance) const
76     {
77         Entity *entity = Node_getEntity(path.top());
78         if (entity != 0
79             && (instance.childSelected() || Instance_getSelectable(instance)->isSelected())) {
80             entity->setKeyValue(m_key, m_value);
81         }
82     }
83 };
84
85 class EntitySetClassnameSelected : public scene::Graph::Walker {
86     const char *m_classname;
87 public:
88     EntitySetClassnameSelected(const char *classname)
89             : m_classname(classname)
90     {
91     }
92
93     bool pre(const scene::Path &path, scene::Instance &instance) const
94     {
95         return true;
96     }
97
98     void post(const scene::Path &path, scene::Instance &instance) const
99     {
100         Entity *entity = Node_getEntity(path.top());
101         if (entity != 0
102             && (instance.childSelected() || Instance_getSelectable(instance)->isSelected())) {
103             NodeSmartReference node(GlobalEntityCreator().createEntity(
104                     GlobalEntityClassManager().findOrInsert(m_classname, node_is_group(path.top()))));
105
106             EntityCopyingVisitor visitor(*Node_getEntity(node));
107
108             entity->forEachKeyValue(visitor);
109
110             NodeSmartReference child(path.top().get());
111             NodeSmartReference parent(path.parent().get());
112             Node_getTraversable(parent)->erase(child);
113             if (Node_getTraversable(child) != 0
114                 && Node_getTraversable(node) != 0
115                 && node_is_group(node)) {
116                 parentBrushes(child, node);
117             }
118             Node_getTraversable(parent)->insert(node);
119         }
120     }
121 };
122
123 void Scene_EntitySetKeyValue_Selected(const char *key, const char *value)
124 {
125     GlobalSceneGraph().traverse(EntitySetKeyValueSelected(key, value));
126 }
127
128 void Scene_EntitySetClassname_Selected(const char *classname)
129 {
130     GlobalSceneGraph().traverse(EntitySetClassnameSelected(classname));
131 }
132
133
134 void Entity_ungroupSelected()
135 {
136     if (GlobalSelectionSystem().countSelected() < 1) {
137         return;
138     }
139
140     UndoableCommand undo("ungroupSelectedEntities");
141
142     scene::Path world_path(makeReference(GlobalSceneGraph().root()));
143     world_path.push(makeReference(Map_FindOrInsertWorldspawn(g_map)));
144
145     scene::Instance &instance = GlobalSelectionSystem().ultimateSelected();
146     scene::Path path = instance.path();
147
148     if (!Node_isEntity(path.top())) {
149         path.pop();
150     }
151
152     if (Node_getEntity(path.top()) != 0
153         && node_is_group(path.top())) {
154         if (world_path.top().get_pointer() != path.top().get_pointer()) {
155             parentBrushes(path.top(), world_path.top());
156             Path_deleteTop(path);
157         }
158     }
159 }
160
161
162 class EntityFindSelected : public scene::Graph::Walker {
163 public:
164     mutable const scene::Path *groupPath;
165     mutable scene::Instance *groupInstance;
166
167     EntityFindSelected() : groupPath(0), groupInstance(0)
168     {
169     }
170
171     bool pre(const scene::Path &path, scene::Instance &instance) const
172     {
173         return true;
174     }
175
176     void post(const scene::Path &path, scene::Instance &instance) const
177     {
178         Entity *entity = Node_getEntity(path.top());
179         if (entity != 0
180             && Instance_getSelectable(instance)->isSelected()
181             && node_is_group(path.top())
182             && !groupPath) {
183             groupPath = &path;
184             groupInstance = &instance;
185         }
186     }
187 };
188
189 class EntityGroupSelected : public scene::Graph::Walker {
190     NodeSmartReference group, worldspawn;
191 //typedef std::pair<NodeSmartReference, NodeSmartReference> DeletionPair;
192 //Stack<DeletionPair> deleteme;
193 public:
194     EntityGroupSelected(const scene::Path &p) : group(p.top().get()), worldspawn(Map_FindOrInsertWorldspawn(g_map))
195     {
196     }
197
198     bool pre(const scene::Path &path, scene::Instance &instance) const
199     {
200         return true;
201     }
202
203     void post(const scene::Path &path, scene::Instance &instance) const
204     {
205         Selectable *selectable = Instance_getSelectable(instance);
206         if (selectable && selectable->isSelected()) {
207             Entity *entity = Node_getEntity(path.top());
208             if (entity == 0 && Node_isPrimitive(path.top())) {
209                 NodeSmartReference child(path.top().get());
210                 NodeSmartReference parent(path.parent().get());
211
212                 if (path.size() >= 3 && parent != worldspawn) {
213                     NodeSmartReference parentparent(path[path.size() - 3].get());
214
215                     Node_getTraversable(parent)->erase(child);
216                     Node_getTraversable(group)->insert(child);
217
218                     if (Node_getTraversable(parent)->empty()) {
219                         //deleteme.push(DeletionPair(parentparent, parent));
220                         Node_getTraversable(parentparent)->erase(parent);
221                     }
222                 } else {
223                     Node_getTraversable(parent)->erase(child);
224                     Node_getTraversable(group)->insert(child);
225                 }
226             }
227         }
228     }
229 };
230
231 void Entity_groupSelected()
232 {
233     if (GlobalSelectionSystem().countSelected() < 1) {
234         return;
235     }
236
237     UndoableCommand undo("groupSelectedEntities");
238
239     scene::Path world_path(makeReference(GlobalSceneGraph().root()));
240     world_path.push(makeReference(Map_FindOrInsertWorldspawn(g_map)));
241
242     EntityFindSelected fse;
243     GlobalSceneGraph().traverse(fse);
244     if (fse.groupPath) {
245         GlobalSceneGraph().traverse(EntityGroupSelected(*fse.groupPath));
246     } else {
247         GlobalSceneGraph().traverse(EntityGroupSelected(world_path));
248     }
249 }
250
251
252 void Entity_connectSelected()
253 {
254     if (GlobalSelectionSystem().countSelected() == 2) {
255         GlobalEntityCreator().connectEntities(
256                 GlobalSelectionSystem().penultimateSelected().path(),
257                 GlobalSelectionSystem().ultimateSelected().path(),
258                 0
259         );
260     } else {
261         globalErrorStream() << "entityConnectSelected: exactly two instances must be selected\n";
262     }
263 }
264
265 void Entity_killconnectSelected()
266 {
267     if (GlobalSelectionSystem().countSelected() == 2) {
268         GlobalEntityCreator().connectEntities(
269                 GlobalSelectionSystem().penultimateSelected().path(),
270                 GlobalSelectionSystem().ultimateSelected().path(),
271                 1
272         );
273     } else {
274         globalErrorStream() << "entityKillConnectSelected: exactly two instances must be selected\n";
275     }
276 }
277
278 AABB Doom3Light_getBounds(const AABB &workzone)
279 {
280     AABB aabb(workzone);
281
282     Vector3 defaultRadius(300, 300, 300);
283     if (!string_parse_vector3(
284             EntityClass_valueForKey(*GlobalEntityClassManager().findOrInsert("light", false), "light_radius"),
285             defaultRadius)) {
286         globalErrorStream() << "Doom3Light_getBounds: failed to parse default light radius\n";
287     }
288
289     if (aabb.extents[0] == 0) {
290         aabb.extents[0] = defaultRadius[0];
291     }
292     if (aabb.extents[1] == 0) {
293         aabb.extents[1] = defaultRadius[1];
294     }
295     if (aabb.extents[2] == 0) {
296         aabb.extents[2] = defaultRadius[2];
297     }
298
299     if (aabb_valid(aabb)) {
300         return aabb;
301     }
302     return AABB(Vector3(0, 0, 0), Vector3(64, 64, 64));
303 }
304
305 int g_iLastLightIntensity;
306
307 void Entity_createFromSelection(const char *name, const Vector3 &origin)
308 {
309 #if 0
310     if ( string_equal_nocase( name, "worldspawn" ) ) {
311         ui::alert( MainFrame_getWindow( ), "Can't create an entity with worldspawn.", "info" );
312         return;
313     }
314 #endif
315
316     EntityClass *entityClass = GlobalEntityClassManager().findOrInsert(name, true);
317
318     bool isModel = (string_compare_nocase_n(name, "misc_", 5) == 0 &&
319                     string_equal_nocase(name + string_length(name) - 5, "model")) // misc_*model (also misc_model)
320                    || string_equal_nocase(name, "model_static")
321                    || (GlobalSelectionSystem().countSelected() == 0 && string_equal_nocase(name, "func_static"));
322
323     bool brushesSelected = Scene_countSelectedBrushes(GlobalSceneGraph()) != 0;
324
325     if (!(entityClass->fixedsize || isModel) && !brushesSelected) {
326         globalErrorStream() << "failed to create a group entity - no brushes are selected\n";
327         return;
328     }
329
330     AABB workzone(aabb_for_minmax(Select_getWorkZone().d_work_min, Select_getWorkZone().d_work_max));
331
332
333     NodeSmartReference node(GlobalEntityCreator().createEntity(entityClass));
334
335     Node_getTraversable(GlobalSceneGraph().root())->insert(node);
336
337     scene::Path entitypath(makeReference(GlobalSceneGraph().root()));
338     entitypath.push(makeReference(node.get()));
339     scene::Instance &instance = findInstance(entitypath);
340
341     if (entityClass->fixedsize || (isModel && !brushesSelected)) {
342         Select_Delete();
343
344         Transformable *transform = Instance_getTransformable(instance);
345         if (transform != 0) {
346             transform->setType(TRANSFORM_PRIMITIVE);
347             transform->setTranslation(origin);
348             transform->freezeTransform();
349         }
350
351         GlobalSelectionSystem().setSelectedAll(false);
352
353         Instance_setSelected(instance, true);
354     } else {
355         if (g_pGameDescription->mGameType == "doom3") {
356             Node_getEntity(node)->setKeyValue("model", Node_getEntity(node)->getKeyValue("name"));
357         }
358
359         Scene_parentSelectedBrushesToEntity(GlobalSceneGraph(), node);
360         Scene_forEachChildSelectable(SelectableSetSelected(true), instance.path());
361     }
362
363     // tweaking: when right clic dropping a light entity, ask for light value in a custom dialog box
364     // see SF bug 105383
365
366     if (g_pGameDescription->mGameType == "hl") {
367         // FIXME - Hydra: really we need a combined light AND color dialog for halflife.
368         if (string_equal_nocase(name, "light")
369             || string_equal_nocase(name, "light_environment")
370             || string_equal_nocase(name, "light_spot")) {
371             int intensity = g_iLastLightIntensity;
372
373             if (DoLightIntensityDlg(&intensity) == eIDOK) {
374                 g_iLastLightIntensity = intensity;
375                 char buf[30];
376                 sprintf(buf, "255 255 255 %d", intensity);
377                 Node_getEntity(node)->setKeyValue("_light", buf);
378             }
379         }
380     } else if (string_equal_nocase(name, "light")) {
381         if (g_pGameDescription->mGameType != "doom3") {
382             int intensity = g_iLastLightIntensity;
383
384             if (DoLightIntensityDlg(&intensity) == eIDOK) {
385                 g_iLastLightIntensity = intensity;
386                 char buf[10];
387                 sprintf(buf, "%d", intensity);
388                 Node_getEntity(node)->setKeyValue("light", buf);
389             }
390         } else if (brushesSelected) { // use workzone to set light position/size for doom3 lights, if there are brushes selected
391             AABB bounds(Doom3Light_getBounds(workzone));
392             StringOutputStream key(64);
393             key << bounds.origin[0] << " " << bounds.origin[1] << " " << bounds.origin[2];
394             Node_getEntity(node)->setKeyValue("origin", key.c_str());
395             key.clear();
396             key << bounds.extents[0] << " " << bounds.extents[1] << " " << bounds.extents[2];
397             Node_getEntity(node)->setKeyValue("light_radius", key.c_str());
398         }
399     }
400
401     if (isModel) {
402         const char *model = misc_model_dialog(MainFrame_getWindow());
403         if (model != 0) {
404             Node_getEntity(node)->setKeyValue("model", model);
405         }
406     }
407 }
408
409 #if 0
410 bool DoNormalisedColor( Vector3& color ){
411     if ( !color_dialog( MainFrame_getWindow( ), color ) ) {
412         return false;
413     }
414     /*
415     ** scale colors so that at least one component is at 1.0F
416     */
417
418     float largest = 0.0F;
419
420     if ( color[0] > largest ) {
421         largest = color[0];
422     }
423     if ( color[1] > largest ) {
424         largest = color[1];
425     }
426     if ( color[2] > largest ) {
427         largest = color[2];
428     }
429
430     if ( largest == 0.0F ) {
431         color[0] = 1.0F;
432         color[1] = 1.0F;
433         color[2] = 1.0F;
434     }
435     else
436     {
437         float scaler = 1.0F / largest;
438
439         color[0] *= scaler;
440         color[1] *= scaler;
441         color[2] *= scaler;
442     }
443
444     return true;
445 }
446 #endif
447
448 void NormalizeColor(Vector3 &color)
449 {
450     // scale colors so that at least one component is at 1.0F
451
452     float largest = 0.0F;
453
454     if (color[0] > largest) {
455         largest = color[0];
456     }
457     if (color[1] > largest) {
458         largest = color[1];
459     }
460     if (color[2] > largest) {
461         largest = color[2];
462     }
463
464     if (largest == 0.0F) {
465         color[0] = 1.0F;
466         color[1] = 1.0F;
467         color[2] = 1.0F;
468     } else {
469         float scaler = 1.0F / largest;
470
471         color[0] *= scaler;
472         color[1] *= scaler;
473         color[2] *= scaler;
474     }
475 }
476
477 void Entity_normalizeColor()
478 {
479     if (GlobalSelectionSystem().countSelected() != 0) {
480         const scene::Path &path = GlobalSelectionSystem().ultimateSelected().path();
481         Entity *entity = Node_getEntity(path.top());
482
483         if (entity != 0) {
484             const char *strColor = entity->getKeyValue("_color");
485             if (!string_empty(strColor)) {
486                 Vector3 rgb;
487                 if (string_parse_vector3(strColor, rgb)) {
488                     g_entity_globals.color_entity = rgb;
489                     NormalizeColor(g_entity_globals.color_entity);
490
491                     char buffer[128];
492                     sprintf(buffer, "%g %g %g", g_entity_globals.color_entity[0],
493                             g_entity_globals.color_entity[1],
494                             g_entity_globals.color_entity[2]);
495
496                     Scene_EntitySetKeyValue_Selected("_color", buffer);
497                 }
498             }
499         }
500     }
501 }
502
503 void Entity_setColour()
504 {
505     if (GlobalSelectionSystem().countSelected() != 0) {
506         bool normalize = false;
507         const scene::Path &path = GlobalSelectionSystem().ultimateSelected().path();
508         Entity *entity = Node_getEntity(path.top());
509
510         if (entity != 0) {
511             const char *strColor = entity->getKeyValue("_color");
512             if (!string_empty(strColor)) {
513                 Vector3 rgb;
514                 if (string_parse_vector3(strColor, rgb)) {
515                     g_entity_globals.color_entity = rgb;
516                 }
517             }
518
519             if (g_pGameDescription->mGameType == "doom3") {
520                 normalize = false;
521             }
522
523             if (color_dialog(MainFrame_getWindow(), g_entity_globals.color_entity)) {
524                 if (normalize) {
525                     NormalizeColor(g_entity_globals.color_entity);
526                 }
527
528                 char buffer[128];
529                 sprintf(buffer, "%g %g %g", g_entity_globals.color_entity[0],
530                         g_entity_globals.color_entity[1],
531                         g_entity_globals.color_entity[2]);
532
533                 Scene_EntitySetKeyValue_Selected("_color", buffer);
534             }
535         }
536     }
537 }
538
539 const char *misc_model_dialog(ui::Widget parent)
540 {
541     StringOutputStream buffer(1024);
542
543     buffer << g_qeglobals.m_userGamePath.c_str() << "models/";
544
545     if (!file_readable(buffer.c_str())) {
546         // just go to fsmain
547         buffer.clear();
548         buffer << g_qeglobals.m_userGamePath.c_str() << "/";
549     }
550
551     const char *filename = parent.file_dialog(TRUE, "Choose Model", buffer.c_str(), ModelLoader::Name());
552     if (filename != 0) {
553         // use VFS to get the correct relative path
554         const char *relative = path_make_relative(filename, GlobalFileSystem().findRoot(filename));
555         if (relative == filename) {
556             globalOutputStream() << "WARNING: could not extract the relative path, using full path instead\n";
557         }
558         return relative;
559     }
560     return 0;
561 }
562
563 struct LightRadii {
564     static void Export(const EntityCreator &self, const Callback<void(bool)> &returnz)
565     {
566         returnz(self.getLightRadii());
567     }
568
569     static void Import(EntityCreator &self, bool value)
570     {
571         self.setLightRadii(value);
572     }
573 };
574
575 void Entity_constructPreferences(PreferencesPage &page)
576 {
577     page.appendCheckBox(
578             "Show", "Light Radii",
579             make_property<LightRadii>(GlobalEntityCreator())
580     );
581 }
582
583 void Entity_constructPage(PreferenceGroup &group)
584 {
585     PreferencesPage page(group.createPage("Entities", "Entity Display Preferences"));
586     Entity_constructPreferences(page);
587 }
588
589 void Entity_registerPreferencesPage()
590 {
591     PreferencesDialog_addDisplayPage(makeCallbackF(Entity_constructPage));
592 }
593
594
595 void Entity_constructMenu(ui::Menu menu)
596 {
597     create_menu_item_with_mnemonic(menu, "_Regroup", "GroupSelection");
598     create_menu_item_with_mnemonic(menu, "_Ungroup", "UngroupSelection");
599     create_menu_item_with_mnemonic(menu, "_Connect", "ConnectSelection");
600     create_menu_item_with_mnemonic(menu, "_KillConnect", "KillConnectSelection");
601     create_menu_item_with_mnemonic(menu, "_Select Color...", "EntityColor");
602     create_menu_item_with_mnemonic(menu, "_Normalize Color...", "NormalizeColor");
603 }
604
605
606 #include "preferencesystem.h"
607 #include "stringio.h"
608
609 void Entity_Construct()
610 {
611     GlobalCommands_insert("EntityColor", makeCallbackF(Entity_setColour), Accelerator('K'));
612     GlobalCommands_insert("NormalizeColor", makeCallbackF(Entity_normalizeColor));
613     GlobalCommands_insert("ConnectSelection", makeCallbackF(Entity_connectSelected),
614                           Accelerator('K', (GdkModifierType) GDK_CONTROL_MASK));
615     GlobalCommands_insert("KillConnectSelection", makeCallbackF(Entity_killconnectSelected),
616                           Accelerator('K', (GdkModifierType) (GDK_SHIFT_MASK)));
617     GlobalCommands_insert("GroupSelection", makeCallbackF(Entity_groupSelected));
618     GlobalCommands_insert("UngroupSelection", makeCallbackF(Entity_ungroupSelected));
619
620     GlobalPreferenceSystem().registerPreference("SI_Colors5", make_property_string(g_entity_globals.color_entity));
621     GlobalPreferenceSystem().registerPreference("LastLightIntensity", make_property_string(g_iLastLightIntensity));
622
623     Entity_registerPreferencesPage();
624 }
625
626 void Entity_Destroy()
627 {
628 }