]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - contrib/sunplug/sunplug.cpp
Merge branch 'polishing' into 'master'
[xonotic/netradiant.git] / contrib / sunplug / sunplug.cpp
1 /*
2    Sunplug plugin for GtkRadiant
3    Copyright (C) 2004 Topsun
4    Thanks to SPoG for help!
5
6    This library is free software; you can redistribute it and/or
7    modify it under the terms of the GNU Lesser General Public
8    License as published by the Free Software Foundation; either
9    version 2.1 of the License, or (at your option) any later version.
10
11    This library is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14    Lesser General Public License for more details.
15
16    You should have received a copy of the GNU Lesser General Public
17    License along with this library; if not, write to the Free Software
18    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20
21 #include "sunplug.h"
22 #include "globaldefs.h"
23
24 #include "debugging/debugging.h"
25
26 #include "iplugin.h"
27
28 #include "string/string.h"
29 #include "modulesystem/singletonmodule.h"
30
31 #include "iundo.h"       // declaration of undo system
32 #include "ientity.h"     // declaration of entity system
33 #include "iscenegraph.h" // declaration of datastructure of the map
34
35 #include "scenelib.h"    // declaration of datastructure of the map
36 #include "qerplugin.h"   // declaration to use other interfaces as a plugin
37
38 #include <gtk/gtk.h>     // to display something with gtk (windows, buttons etc.), the whole package might not be necessary
39
40 #define CMD_ABOUT "About..."
41
42 void about_plugin_window();
43 void MapCoordinator();
44
45 #if !GDEF_OS_WINDOWS
46 // linux itoa implementation
47 char* itoa( int value, char* result, int base ){
48         // check that the base if valid
49         if ( base < 2 || base > 16 ) {
50                 *result = 0;
51                 return result;
52         }
53
54         char* out = result;
55         int quotient = value;
56
57         do
58         {
59                 *out = "0123456789abcdef"[abs( quotient % base )];
60                 ++out;
61
62                 quotient /= base;
63         } while ( quotient );
64
65         // Only apply negative sign for base 10
66         if ( value < 0 && base == 10 ) {
67                 *out++ = '-';
68         }
69
70         std::reverse( result, out );
71
72         *out = 0;
73         return result;
74 }
75 #endif
76
77 typedef struct _mapcoord_setting_packet {
78     ui::SpinButton spinner1{ui::null}, spinner2{ui::null}, spinner3{ui::null}, spinner4{ui::null};
79         Entity* worldspawn;
80 } mapcoord_setting_packet;
81
82 static int map_minX, map_maxX, map_minY, map_maxY;
83 static int minX, maxX, minY, maxY;
84 mapcoord_setting_packet msp;
85
86 //  **************************
87 // ** find entities by class **  from radiant/map.cpp
88 //  **************************
89 class EntityFindByClassname : public scene::Graph::Walker
90 {
91 const char* m_name;
92 Entity*& m_entity;
93 public:
94 EntityFindByClassname( const char* name, Entity*& entity ) : m_name( name ), m_entity( entity ){
95         m_entity = 0;
96 }
97 bool pre( const scene::Path& path, scene::Instance& instance ) const {
98         if ( m_entity == 0 ) {
99                 Entity* entity = Node_getEntity( path.top() );
100                 if ( entity != 0
101                          && string_equal( m_name, entity->getKeyValue( "classname" ) ) ) {
102                         m_entity = entity;
103                 }
104         }
105         return true;
106 }
107 };
108
109 Entity* Scene_FindEntityByClass( const char* name ){
110         Entity* entity;
111         GlobalSceneGraph().traverse( EntityFindByClassname( name, entity ) );
112         return entity;
113 }
114
115 //  **************************
116 // ** GTK callback functions **
117 //  **************************
118
119 static gboolean delete_event(ui::Widget widget, GdkEvent *event, gpointer data ){
120         /* If you return FALSE in the "delete_event" signal handler,
121          * GTK will emit the "destroy" signal. Returning TRUE means
122          * you don't want the window to be destroyed.
123          * This is useful for popping up 'are you sure you want to quit?'
124          * type dialogs. */
125
126         return FALSE;
127 }
128
129 // destroy widget if destroy signal is passed to widget
130 static void destroy( ui::Widget widget, gpointer data ){
131         widget.destroy();
132 }
133
134 // function for close button to destroy the toplevel widget
135 static void close_window( ui::Widget widget, gpointer data ){
136         widget.window().destroy();
137 }
138
139 // callback function to assign the optimal mapcoords to the spinboxes
140 static void input_optimal(ui::Widget widget, gpointer data ){
141         gtk_spin_button_set_value( msp.spinner1, minX );
142         gtk_spin_button_set_value( msp.spinner2, maxY );
143         gtk_spin_button_set_value( msp.spinner3, maxX );
144         gtk_spin_button_set_value( msp.spinner4, minY );
145 }
146
147 // Spinner return value function
148 gint grab_int_value(ui::SpinButton a_spinner, gpointer user_data ) {
149         return gtk_spin_button_get_value_as_int( a_spinner );
150 }
151
152 // write the values of the Spinner-Boxes to the worldspawn
153 static void set_coordinates( ui::Widget widget, gpointer data ){
154         //Str str_min, str_max;
155         char buffer[10], str_min[20], str_max[20];
156
157         itoa( gtk_spin_button_get_value_as_int( msp.spinner1 ), str_min, 10 );
158         itoa( gtk_spin_button_get_value_as_int( msp.spinner2 ), buffer, 10 );
159         strcat( str_min, " " );
160         strcat( str_min, buffer );
161         msp.worldspawn->setKeyValue( "mapcoordsmins", str_min );
162
163         itoa( gtk_spin_button_get_value_as_int( msp.spinner3 ), str_max, 10 );
164         itoa( gtk_spin_button_get_value_as_int( msp.spinner4 ), buffer, 10 );
165         strcat( str_max, " " );
166         strcat( str_max, buffer );
167         UndoableCommand undo( "SunPlug.entitySetMapcoords" );
168         msp.worldspawn->setKeyValue( "mapcoordsmaxs", str_max );
169
170         close_window( widget, NULL );
171 }
172
173 class SunPlugPluginDependencies :
174         public GlobalRadiantModuleRef,  // basic class for all other module refs
175         public GlobalUndoModuleRef,     // used to say radiant that something has changed and to undo that
176         public GlobalSceneGraphModuleRef, // necessary to handle data in the mapfile (change, retrieve data)
177         public GlobalEntityModuleRef    // to access and modify the entities
178 {
179 public:
180 SunPlugPluginDependencies() :
181         GlobalEntityModuleRef( GlobalRadiant().getRequiredGameDescriptionKeyValue( "entities" ) ){ //,
182 }
183 };
184
185 //  *************************
186 // ** standard plugin stuff **
187 //  *************************
188 namespace SunPlug
189 {
190 ui::Window main_window{ui::null};
191 char MenuList[100] = "";
192
193 const char* init( void* hApp, void* pMainWidget ){
194         main_window = ui::Window::from(pMainWidget);
195         return "Initializing " PLUGIN_NAME " for " RADIANT_NAME;
196 }
197 const char* getName(){
198         return PLUGIN_NAME; // name that is shown in the menue
199 }
200 const char* getCommandList(){
201         const char about[] = CMD_ABOUT;
202         const char etMapCoordinator[] = ";ET-MapCoordinator";
203
204         strcat( MenuList, about );
205         if ( strncmp( GlobalRadiant().getGameName(), "etmain", 6 ) == 0 ) {
206                 strcat( MenuList, etMapCoordinator );
207         }
208         return (const char*)MenuList;
209 }
210 const char* getCommandTitleList(){
211         return "";
212 }
213 void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush ){ // message processing
214         if ( string_equal( command, CMD_ABOUT ) ) {
215                 about_plugin_window();
216         }
217         if ( string_equal( command, "ET-MapCoordinator" ) ) {
218                 MapCoordinator();
219         }
220 }
221 } // namespace
222
223 class SunPlugModule : public TypeSystemRef
224 {
225 _QERPluginTable m_plugin;
226 public:
227 typedef _QERPluginTable Type;
228 STRING_CONSTANT( Name, PLUGIN_NAME );
229
230 SunPlugModule(){
231         m_plugin.m_pfnQERPlug_Init = &SunPlug::init;
232         m_plugin.m_pfnQERPlug_GetName = &SunPlug::getName;
233         m_plugin.m_pfnQERPlug_GetCommandList = &SunPlug::getCommandList;
234         m_plugin.m_pfnQERPlug_GetCommandTitleList = &SunPlug::getCommandTitleList;
235         m_plugin.m_pfnQERPlug_Dispatch = &SunPlug::dispatch;
236 }
237 _QERPluginTable* getTable(){
238         return &m_plugin;
239 }
240 };
241
242 typedef SingletonModule<SunPlugModule, SunPlugPluginDependencies> SingletonSunPlugModule;
243
244 SingletonSunPlugModule g_SunPlugModule;
245
246
247 extern "C" void RADIANT_DLLEXPORT Radiant_RegisterModules( ModuleServer& server ){
248         initialiseModule( server );
249
250         g_SunPlugModule.selfRegister();
251 }
252
253 //  ************
254 // ** my stuff **
255 //  ************
256
257 // About dialog
258 void about_plugin_window(){
259         auto window = ui::Window( ui::window_type::TOP ); // create a window
260         gtk_window_set_transient_for( window, SunPlug::main_window ); // make the window to stay in front of the main window
261         window.connect( "delete_event", G_CALLBACK( delete_event ), NULL ); // connect the delete event
262         window.connect( "destroy", G_CALLBACK( destroy ), NULL ); // connect the destroy event for the window
263         gtk_window_set_title( window, "About " PLUGIN_NAME ); // set the title of the window for the window
264         gtk_window_set_resizable( window, FALSE ); // don't let the user resize the window
265         gtk_window_set_modal( window, TRUE ); // force the user not to do something with the other windows
266         gtk_container_set_border_width( GTK_CONTAINER( window ), 10 ); // set the border of the window
267
268         auto vbox = ui::VBox( FALSE, 10 ); // create a box to arrange new objects vertically
269         window.add(vbox);
270
271         char const *label_text = 
272         PLUGIN_NAME " " PLUGIN_VERSION " for "
273         RADIANT_NAME " " RADIANT_VERSION "\n\n"
274         "Written by Topsun\n\n"
275         "Built against "
276         RADIANT_NAME " " RADIANT_VERSION_STRING "\n"
277         __DATE__;
278
279         auto label = ui::Label( label_text ); // create a label
280         gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); // text align left
281         vbox.pack_start( label, FALSE, FALSE, 2 ); // insert the label in the box
282
283         auto button = ui::Button( "OK" ); // create a button with text
284         g_signal_connect_swapped( G_OBJECT( button ), "clicked", G_CALLBACK( gtk_widget_destroy ), (void *) window ); // connect the click event to close the window
285         vbox.pack_start( button, FALSE, FALSE, 2 ); // insert the button in the box
286
287         gtk_window_set_position( window, GTK_WIN_POS_CENTER ); // center the window on screen
288
289         gtk_widget_show_all( window ); // show the window and all subelements
290 }
291
292 // get the current bounding box and return the optimal coordinates
293 void GetOptimalCoordinates( AABB *levelBoundingBox ){
294         int half_width, half_heigth, center_x, center_y;
295
296         half_width = levelBoundingBox->extents.x();
297         half_heigth = levelBoundingBox->extents.y();
298         center_x = levelBoundingBox->origin.x();
299         center_y = levelBoundingBox->origin.y();
300
301         if ( half_width > 175 || half_heigth > 175 ) { // the square must be at least 350x350 units
302                 // the wider side is the indicator for the square
303                 if ( half_width >= half_heigth ) {
304                         minX = center_x - half_width;
305                         maxX = center_x + half_width;
306                         minY = center_y - half_width;
307                         maxY = center_y + half_width;
308                 }
309                 else {
310                         minX = center_x - half_heigth;
311                         maxX = center_x + half_heigth;
312                         minY = center_y - half_heigth;
313                         maxY = center_y + half_heigth;
314                 }
315         }
316         else {
317                 minX = center_x - 175;
318                 maxX = center_x + 175;
319                 minY = center_y - 175;
320                 maxY = center_y + 175;
321         }
322 }
323
324 // MapCoordinator dialog window
325 void MapCoordinator(){
326         Entity *theWorldspawn = NULL;
327         const char *buffer;
328         char line[20];
329
330         // in any case we need a window to show the user what to do
331         auto window = ui::Window( ui::window_type::TOP ); // create the window
332         gtk_window_set_transient_for( window, SunPlug::main_window ); // make the window to stay in front of the main window
333         window.connect( "delete_event", G_CALLBACK( delete_event ), NULL ); // connect the delete event for the window
334         window.connect( "destroy", G_CALLBACK( destroy ), NULL ); // connect the destroy event for the window
335         gtk_window_set_title( window, "ET-MapCoordinator" ); // set the title of the window for the window
336         gtk_window_set_resizable( window, FALSE ); // don't let the user resize the window
337         gtk_window_set_modal( window, TRUE ); // force the user not to do something with the other windows
338         gtk_container_set_border_width( GTK_CONTAINER( window ), 10 ); // set the border of the window
339
340         auto vbox = ui::VBox( FALSE, 10 ); // create a box to arrange new objects vertically
341         window.add(vbox);
342
343         scene::Path path = makeReference( GlobalSceneGraph().root() ); // get the path to the root element of the graph
344         scene::Instance* instance = GlobalSceneGraph().find( path ); // find the instance to the given path
345         AABB levelBoundingBox = instance->worldAABB(); // get the bounding box of the level
346
347         theWorldspawn = Scene_FindEntityByClass( "worldspawn" ); // find the entity worldspawn
348         if ( theWorldspawn != 0 ) { // need to have a worldspawn otherwise setting a value crashes the radiant
349                 // next two if's: get the current values of the mapcoords
350                 buffer = theWorldspawn->getKeyValue( "mapcoordsmins" ); // upper left corner
351                 if ( strlen( buffer ) > 0 ) {
352                         strncpy( line, buffer, 19 );
353                         map_minX = atoi( strtok( line, " " ) ); // minimum of x value
354                         map_minY = atoi( strtok( NULL, " " ) ); // maximum of y value
355                 }
356                 else {
357                         map_minX = 0;
358                         map_minY = 0;
359                 }
360                 buffer = theWorldspawn->getKeyValue( "mapcoordsmaxs" ); // lower right corner
361                 if ( strlen( buffer ) > 0 ) {
362                         strncpy( line, buffer, 19 );
363                         map_maxX = atoi( strtok( line, " " ) ); // maximum of x value
364                         map_maxY = atoi( strtok( NULL, " " ) ); // minimum of y value
365                 }
366                 else {
367                         map_maxX = 0;
368                         map_maxY = 0;
369                 }
370
371                 globalOutputStream() << "SunPlug: calculating optimal coordinates\n"; // write to console that we are calculating the coordinates
372                 GetOptimalCoordinates( &levelBoundingBox ); // calculate optimal mapcoords with the dimensions of the level bounding box
373                 globalOutputStream() << "SunPlug: adviced mapcoordsmins=" << minX << " " << maxY << "\n"; // console info about mapcoordsmins
374                 globalOutputStream() << "SunPlug: adviced mapcoordsmaxs=" << maxX << " " << minY << "\n"; // console info about mapcoordsmaxs
375
376                 auto spinner_adj_MinX = ui::Adjustment( map_minX, -65536.0, 65536.0, 1.0, 5.0, 0 ); // create adjustment for value and range of minimum x value
377                 auto spinner_adj_MinY = ui::Adjustment( map_minY, -65536.0, 65536.0, 1.0, 5.0, 0 ); // create adjustment for value and range of minimum y value
378                 auto spinner_adj_MaxX = ui::Adjustment( map_maxX, -65536.0, 65536.0, 1.0, 5.0, 0 ); // create adjustment for value and range of maximum x value
379                 auto spinner_adj_MaxY = ui::Adjustment( map_maxY, -65536.0, 65536.0, 1.0, 5.0, 0 ); // create adjustment for value and range of maximum y value
380
381                 auto button = ui::Button( "Get optimal mapcoords" ); // create button with text
382                 button.connect( "clicked", G_CALLBACK( input_optimal ), NULL ); // connect button with callback function
383                 vbox.pack_start( button, FALSE, FALSE, 2 ); // insert button into vbox
384
385                 vbox.pack_start( ui::Widget::from(gtk_hseparator_new()), FALSE, FALSE, 2 ); // insert separator into vbox
386
387                 auto table = ui::Table( 4, 3, TRUE ); // create table
388         gtk_table_set_row_spacings(table, 8); // set row spacings
389         gtk_table_set_col_spacings(table, 8); // set column spacings
390                 vbox.pack_start( table, FALSE, FALSE, 2 ); // insert table into vbox
391
392                 auto label = ui::Label( "x" ); // create label
393                 gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); // align text to the left side
394         table.attach(label, {1, 2, 0, 1}); // insert label into table
395
396                 label = ui::Label( "y" ); // create label
397                 gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); // align text to the left side
398         table.attach(label, {2, 3, 0, 1}); // insert label into table
399
400                 label = ui::Label( "mapcoordsmins" ); // create label
401                 gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); // align text to the left side
402         table.attach(label, {0, 1, 1, 2}); // insert label into table
403
404         auto spinnerMinX = ui::SpinButton(spinner_adj_MinX, 1.0, 0); // create textbox wiht value spin, value and value range
405         table.attach(spinnerMinX, {1, 2, 1, 2}); // insert spinbox into table
406
407         auto spinnerMinY = ui::SpinButton(spinner_adj_MinY, 1.0, 0); // create textbox wiht value spin, value and value range
408         table.attach(spinnerMinY, {2, 3, 1, 2}); // insert spinbox into table
409
410                 label = ui::Label( "mapcoordsmaxs" ); // create label
411                 gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); // align text to the left side
412         table.attach(label, {0, 1, 2, 3}); // insert label into table
413
414         auto spinnerMaxX = ui::SpinButton(spinner_adj_MaxX, 1.0, 0); // create textbox wiht value spin, value and value range
415         table.attach(spinnerMaxX, {1, 2, 2, 3}); // insert spinbox into table
416
417         auto spinnerMaxY = ui::SpinButton(spinner_adj_MaxY, 1.0, 0); // create textbox wiht value spin, value and value range
418         table.attach(spinnerMaxY, {2, 3, 2, 3}); // insert spinbox into table
419
420                 // put the references to the spinboxes and the worldspawn into the global exchange
421         msp.spinner1 = spinnerMinX;
422         msp.spinner2 = spinnerMinY;
423         msp.spinner3 = spinnerMaxX;
424         msp.spinner4 = spinnerMaxY;
425                 msp.worldspawn = theWorldspawn;
426
427                 button = ui::Button( "Set" ); // create button with text
428                 button.connect( "clicked", G_CALLBACK( set_coordinates ), NULL ); // connect button with callback function
429         table.attach(button, {1, 2, 3, 4}); // insert button into table
430
431                 button = ui::Button( "Cancel" ); // create button with text
432                 button.connect( "clicked", G_CALLBACK( close_window ), NULL ); // connect button with callback function
433         table.attach(button, {2, 3, 3, 4}); // insert button into table
434         }
435         else {
436                 globalOutputStream() << "SunPlug: no worldspawn found!\n"; // output error to console
437
438                 auto label = ui::Label( "ERROR: No worldspawn was found in the map!\nIn order to use this tool the map must have at least one brush in the worldspawn. " ); // create a label
439                 gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); // text align left
440                 vbox.pack_start( label, FALSE, FALSE, 2 ); // insert the label in the box
441
442                 auto button = ui::Button( "OK" ); // create a button with text
443                 button.connect( "clicked", G_CALLBACK( close_window ), NULL ); // connect the click event to close the window
444                 vbox.pack_start( button, FALSE, FALSE, 2 ); // insert the button in the box
445         }
446
447         gtk_window_set_position( window, GTK_WIN_POS_CENTER ); // center the window
448         gtk_widget_show_all( window ); // show the window and all subelements
449 }