1 //  SuperTux -  A Jump'n Run
2 //  Copyright (C) 2006 Matthias Braun <matze@braunis.de>
3 //
4 //  This program is free software: you can redistribute it and/or modify
5 //  it under the terms of the GNU General Public License as published by
6 //  the Free Software Foundation, either version 3 of the License, or
7 //  (at your option) any later version.
8 //
9 //  This program is distributed in the hope that it will be useful,
10 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 //  GNU General Public License for more details.
13 //
14 //  You should have received a copy of the GNU General Public License
15 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 
17 #include "supertux/sector.hpp"
18 
19 #include <physfs.h>
20 #include <algorithm>
21 
22 #include "audio/sound_manager.hpp"
23 #include "badguy/badguy.hpp"
24 #include "collision/collision.hpp"
25 #include "collision/collision_system.hpp"
26 #include "editor/editor.hpp"
27 #include "math/aatriangle.hpp"
28 #include "math/rect.hpp"
29 #include "object/ambient_light.hpp"
30 #include "object/background.hpp"
31 #include "object/bullet.hpp"
32 #include "object/camera.hpp"
33 #include "object/display_effect.hpp"
34 #include "object/gradient.hpp"
35 #include "object/music_object.hpp"
36 #include "object/player.hpp"
37 #include "object/portable.hpp"
38 #include "object/pulsing_light.hpp"
39 #include "object/smoke_cloud.hpp"
40 #include "object/spawnpoint.hpp"
41 #include "object/text_array_object.hpp"
42 #include "object/text_object.hpp"
43 #include "object/tilemap.hpp"
44 #include "object/vertical_stripes.hpp"
45 #include "physfs/ifile_stream.hpp"
46 #include "scripting/sector.hpp"
47 #include "squirrel/squirrel_environment.hpp"
48 #include "supertux/colorscheme.hpp"
49 #include "supertux/constants.hpp"
50 #include "supertux/debug.hpp"
51 #include "supertux/game_object_factory.hpp"
52 #include "supertux/game_session.hpp"
53 #include "supertux/level.hpp"
54 #include "supertux/player_status_hud.hpp"
55 #include "supertux/resources.hpp"
56 #include "supertux/savegame.hpp"
57 #include "supertux/tile.hpp"
58 #include "util/file_system.hpp"
59 #include "util/writer.hpp"
60 #include "video/video_system.hpp"
61 #include "video/viewport.hpp"
62 
63 Sector* Sector::s_current = nullptr;
64 
65 namespace {
66 
67 PlayerStatus dummy_player_status;
68 
69 } // namespace
70 
Sector(Level & parent)71 Sector::Sector(Level& parent) :
72   m_level(parent),
73   m_name(),
74   m_fully_constructed(false),
75   m_init_script(),
76   m_foremost_layer(),
77   m_squirrel_environment(new SquirrelEnvironment(SquirrelVirtualMachine::current()->get_vm(), "sector")),
78   m_collision_system(new CollisionSystem(*this)),
79   m_gravity(10.0)
80 {
81   Savegame* savegame = (Editor::current() && Editor::is_active()) ?
82     Editor::current()->m_savegame.get() :
83     GameSession::current() ? &GameSession::current()->get_savegame() : nullptr;
84   PlayerStatus& player_status = savegame ? savegame->get_player_status() : dummy_player_status;
85 
86   if (savegame && !m_level.m_suppress_pause_menu && !savegame->is_title_screen()) {
87     add<PlayerStatusHUD>(player_status);
88   }
89   add<Player>(player_status, "Tux");
90   add<DisplayEffect>("Effect");
91   add<TextObject>("Text");
92   add<TextArrayObject>("TextArray");
93 
94   SoundManager::current()->preload("sounds/shoot.wav");
95 }
96 
~Sector()97 Sector::~Sector()
98 {
99   try
100   {
101     deactivate();
102   }
103   catch(const std::exception& err)
104   {
105     log_warning << err.what() << std::endl;
106   }
107 
108   clear_objects();
109 }
110 
111 void
finish_construction(bool editable)112 Sector::finish_construction(bool editable)
113 {
114   flush_game_objects();
115 
116   // FIXME: Is it a good idea to process some resolve requests this early?
117   // I added this to fix https://github.com/SuperTux/supertux/issues/1378
118   // but I don't know if it's going to introduce other bugs..   ~ Semphris
119   try_process_resolve_requests();
120 
121   if (!editable) {
122     convert_tiles2gameobject();
123 
124     bool has_background = std::any_of(get_objects().begin(), get_objects().end(),
125                                       [](const auto& obj) {
126                                         return (dynamic_cast<Background*>(obj.get()) ||
127                                                 dynamic_cast<Gradient*>(obj.get()));
128                                       });
129     if (!has_background) {
130       auto& gradient = add<Gradient>();
131       gradient.set_gradient(Color(0.3f, 0.4f, 0.75f), Color(1.f, 1.f, 1.f));
132     }
133   }
134 
135   if (get_solid_tilemaps().empty()) {
136     log_warning << "sector '" << get_name() << "' does not contain a solid tile layer." << std::endl;
137   }
138 
139   if (!get_object_by_type<Camera>()) {
140     log_warning << "sector '" << get_name() << "' does not contain a camera." << std::endl;
141     add<Camera>("Camera");
142   }
143 
144   if (!get_object_by_type<AmbientLight>()) {
145     add<AmbientLight>(Color::WHITE);
146   }
147 
148   if (!get_object_by_type<MusicObject>()) {
149     add<MusicObject>();
150   }
151 
152   if (!get_object_by_type<VerticalStripes>()) {
153     add<VerticalStripes>();
154   }
155 
156   flush_game_objects();
157 
158   m_foremost_layer = calculate_foremost_layer();
159 
160   process_resolve_requests();
161 
162   for (auto& object : get_objects()) {
163     object->finish_construction();
164   }
165 
166   flush_game_objects();
167 
168   m_fully_constructed = true;
169 }
170 
171 Level&
get_level() const172 Sector::get_level() const
173 {
174   return m_level;
175 }
176 
177 void
activate(const std::string & spawnpoint)178 Sector::activate(const std::string& spawnpoint)
179 {
180   SpawnPointMarker* sp = nullptr;
181   for (auto& spawn_point : get_objects_by_type<SpawnPointMarker>()) {
182     if (spawn_point.get_name() == spawnpoint) {
183       sp = &spawn_point;
184       break;
185     }
186   }
187 
188   if (!sp) {
189     log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl;
190     if (spawnpoint != "main") {
191       activate("main");
192     } else {
193       activate(Vector(0, 0));
194     }
195   } else {
196     activate(sp->get_pos());
197   }
198 }
199 
200 void
activate(const Vector & player_pos)201 Sector::activate(const Vector& player_pos)
202 {
203   BIND_SECTOR(*this);
204 
205   if (s_current != this) {
206     if (s_current != nullptr)
207       s_current->deactivate();
208     s_current = this;
209 
210     m_squirrel_environment->expose_self();
211 
212     for (auto& object : get_objects()) {
213       m_squirrel_environment->try_expose(*object);
214     }
215   }
216 
217   // The Sector object is called 'settings' as it is accessed as 'sector.settings'
218   m_squirrel_environment->expose("settings", std::make_unique<scripting::Sector>(this));
219 
220   // two-player hack: move other players to main player's position
221   // Maybe specify 2 spawnpoints in the level?
222   for (auto player_ptr : get_objects_by_type_index(typeid(Player))) {
223     Player& player = *static_cast<Player*>(player_ptr);
224     // spawn smalltux below spawnpoint
225     if (!player.is_big()) {
226       player.move(player_pos + Vector(0,32));
227     } else {
228       player.move(player_pos);
229     }
230 
231     // spawning tux in the ground would kill him
232     if (!is_free_of_tiles(player.get_bbox())) {
233       std::string current_level = "[" + Sector::get().get_level().m_filename + "] ";
234       log_warning << current_level << "Tried spawning Tux in solid matter. Compensating." << std::endl;
235       Vector npos = player.get_bbox().p1();
236       npos.y-=32;
237       player.move(npos);
238     }
239   }
240 
241   { //FIXME: This is a really dirty workaround for this strange camera jump
242     Player& player = get_player();
243     Camera& camera = get_camera();
244     player.move(player.get_pos()+Vector(-32, 0));
245     camera.reset(player.get_pos());
246     camera.update(1);
247     player.move(player.get_pos()+(Vector(32, 0)));
248     camera.update(1);
249   }
250 
251   flush_game_objects();
252 
253   //Run default.nut just before init script
254   //Check to see if it's in a levelset (info file)
255   std::string basedir = FileSystem::dirname(get_level().m_filename);
256   if (PHYSFS_exists((basedir + "/info").c_str())) {
257     try {
258       IFileStream in(basedir + "/default.nut");
259       m_squirrel_environment->run_script(in, "default.nut");
260     } catch(std::exception& ) {
261       // doesn't exist or erroneous; do nothing
262     }
263   }
264 
265   // Run init script
266   if (!m_init_script.empty() && !Editor::is_active()) {
267     run_script(m_init_script, "init-script");
268   }
269 }
270 
271 void
deactivate()272 Sector::deactivate()
273 {
274   BIND_SECTOR(*this);
275 
276   if (s_current != this)
277     return;
278 
279   m_squirrel_environment->unexpose_self();
280 
281   for (const auto& object: get_objects()) {
282     m_squirrel_environment->try_unexpose(*object);
283   }
284 
285   m_squirrel_environment->unexpose("settings");
286 
287   s_current = nullptr;
288 }
289 
290 Rectf
get_active_region() const291 Sector::get_active_region() const
292 {
293   Camera& camera = get_camera();
294   return Rectf(
295     camera.get_translation() - Vector(1600, 1200),
296     camera.get_translation() + Vector(1600, 1200) + Vector(static_cast<float>(SCREEN_WIDTH),
297                                                            static_cast<float>(SCREEN_HEIGHT)));
298 }
299 
300 int
calculate_foremost_layer() const301 Sector::calculate_foremost_layer() const
302 {
303   int layer = LAYER_BACKGROUND0;
304   for (auto& tm : get_objects_by_type<TileMap>())
305   {
306     if (tm.get_layer() > layer)
307     {
308       if ( (tm.get_alpha() < 1.0f) )
309       {
310         layer = tm.get_layer() - 1;
311       }
312       else
313       {
314         layer = tm.get_layer() + 1;
315       }
316     }
317   }
318   log_debug << "Calculated baduy falling layer was: " << layer << std::endl;
319   return layer;
320 }
321 
322 int
get_foremost_layer() const323 Sector::get_foremost_layer() const
324 {
325   return m_foremost_layer;
326 }
327 
328 void
update(float dt_sec)329 Sector::update(float dt_sec)
330 {
331   assert(m_fully_constructed);
332 
333   BIND_SECTOR(*this);
334 
335   m_squirrel_environment->update(dt_sec);
336 
337   GameObjectManager::update(dt_sec);
338 
339   /* Handle all possible collisions. */
340   m_collision_system->update();
341   flush_game_objects();
342 }
343 
344 bool
before_object_add(GameObject & object)345 Sector::before_object_add(GameObject& object)
346 {
347   if (object.is_singleton())
348   {
349     const auto& objects = get_objects_by_type_index(std::type_index(typeid(object)));
350     if (!objects.empty())
351     {
352       log_warning << "Can't insert multiple GameObject of type '" << typeid(object).name() << "', ignoring" << std::endl;
353       return false;
354     }
355   }
356 
357   if (auto* movingobject = dynamic_cast<MovingObject*>(&object))
358   {
359     m_collision_system->add(movingobject->get_collision_object());
360   }
361 
362   if (auto* tilemap = dynamic_cast<TileMap*>(&object))
363   {
364     tilemap->set_ground_movement_manager(m_collision_system->get_ground_movement_manager());
365   }
366 
367   if (s_current == this) {
368     m_squirrel_environment->try_expose(object);
369   }
370 
371   if (m_fully_constructed) {
372     // if the sector is already fully constructed, finish the object
373     // constructions, as there should be no more named references to resolve
374     object.finish_construction();
375   }
376 
377   return true;
378 }
379 
380 void
before_object_remove(GameObject & object)381 Sector::before_object_remove(GameObject& object)
382 {
383   auto moving_object = dynamic_cast<MovingObject*>(&object);
384   if (moving_object) {
385     m_collision_system->remove(moving_object->get_collision_object());
386   }
387 
388   if (s_current == this)
389     m_squirrel_environment->try_unexpose(object);
390 }
391 
392 void
draw(DrawingContext & context)393 Sector::draw(DrawingContext& context)
394 {
395   BIND_SECTOR(*this);
396 
397   Camera& camera = get_camera();
398 
399   context.push_transform();
400   context.set_translation(camera.get_translation());
401   context.scale(camera.get_current_scale());
402 
403   GameObjectManager::draw(context);
404 
405   if (g_debug.show_collision_rects) {
406     m_collision_system->draw(context);
407   }
408 
409   context.pop_transform();
410 
411   if (m_level.m_is_in_cutscene && !m_level.m_skip_cutscene)
412   {
413     context.color().draw_text(Resources::normal_font,
414                               _("Press escape to skip"),
415                               Vector(32.f, 32.f),
416                               ALIGN_LEFT,
417                               LAYER_OBJECTS + 1000,
418                               ColorScheme::Text::heading_color);
419   }
420 }
421 
422 bool
is_free_of_tiles(const Rectf & rect,const bool ignoreUnisolid,uint32_t tiletype) const423 Sector::is_free_of_tiles(const Rectf& rect, const bool ignoreUnisolid, uint32_t tiletype) const
424 {
425   return m_collision_system->is_free_of_tiles(rect, ignoreUnisolid, tiletype);
426 }
427 
428 bool
is_free_of_statics(const Rectf & rect,const MovingObject * ignore_object,const bool ignoreUnisolid) const429 Sector::is_free_of_statics(const Rectf& rect, const MovingObject* ignore_object, const bool ignoreUnisolid) const
430 {
431   return m_collision_system->is_free_of_statics(rect,
432                                                 ignore_object ? ignore_object->get_collision_object() : nullptr,
433                                                 ignoreUnisolid);
434 }
435 
436 bool
is_free_of_movingstatics(const Rectf & rect,const MovingObject * ignore_object) const437 Sector::is_free_of_movingstatics(const Rectf& rect, const MovingObject* ignore_object) const
438 {
439   return m_collision_system->is_free_of_movingstatics(rect,
440                                                       ignore_object ? ignore_object->get_collision_object() : nullptr);
441 }
442 
443 bool
free_line_of_sight(const Vector & line_start,const Vector & line_end,bool ignore_objects,const MovingObject * ignore_object) const444 Sector::free_line_of_sight(const Vector& line_start, const Vector& line_end, bool ignore_objects, const MovingObject* ignore_object) const
445 {
446   return m_collision_system->free_line_of_sight(line_start, line_end, ignore_objects,
447                                                 ignore_object ? ignore_object->get_collision_object() : nullptr);
448 }
449 
450 bool
can_see_player(const Vector & eye) const451 Sector::can_see_player(const Vector& eye) const
452 {
453   for (auto player_ptr : get_objects_by_type_index(typeid(Player))) {
454     Player& player = *static_cast<Player*>(player_ptr);
455     // test for free line of sight to any of all four corners and the middle of the player's bounding box
456     if (free_line_of_sight(eye, player.get_bbox().p1(), false, &player)) return true;
457     if (free_line_of_sight(eye, Vector(player.get_bbox().get_right(), player.get_bbox().get_top()), false, &player)) return true;
458     if (free_line_of_sight(eye, player.get_bbox().p2(), false, &player)) return true;
459     if (free_line_of_sight(eye, Vector(player.get_bbox().get_left(), player.get_bbox().get_bottom()), false, &player)) return true;
460     if (free_line_of_sight(eye, player.get_bbox().get_middle(), false, &player)) return true;
461   }
462   return false;
463 }
464 
465 bool
inside(const Rectf & rect) const466 Sector::inside(const Rectf& rect) const
467 {
468   for (const auto& solids : get_solid_tilemaps()) {
469     Rectf bbox = solids->get_bbox();
470 
471     // the top of the sector extends to infinity
472     if (bbox.get_left() <= rect.get_left() &&
473         rect.get_right() <= bbox.get_right() &&
474         rect.get_bottom() <= bbox.get_bottom()) {
475       return true;
476     }
477   }
478   return false;
479 }
480 
481 Size
get_editor_size() const482 Sector::get_editor_size() const
483 {
484   // Find the solid tilemap with the greatest surface
485   size_t max_surface = 0;
486   Size size;
487   for (const auto& solids: get_solid_tilemaps()) {
488     size_t surface = solids->get_width() * solids->get_height();
489     if (surface > max_surface) {
490       max_surface = surface;
491       size = solids->get_size();
492     }
493   }
494 
495   return size;
496 }
497 
498 void
resize_sector(const Size & old_size,const Size & new_size,const Size & resize_offset)499 Sector::resize_sector(const Size& old_size, const Size& new_size, const Size& resize_offset)
500 {
501   BIND_SECTOR(*this);
502 
503   bool is_offset = resize_offset.width || resize_offset.height;
504   Vector obj_shift = Vector(static_cast<float>(resize_offset.width) * 32.0f,
505                             static_cast<float>(resize_offset.height) * 32.0f);
506   for (const auto& object : get_objects()) {
507     auto tilemap = dynamic_cast<TileMap*>(object.get());
508     if (tilemap) {
509       if (tilemap->get_size() == old_size) {
510         tilemap->resize(new_size, resize_offset);
511       } else if (is_offset) {
512         tilemap->move_by(obj_shift);
513       }
514     } else if (is_offset) {
515       auto moving_object = dynamic_cast<MovingObject*>(object.get());
516       if (moving_object) {
517         moving_object->move_to(moving_object->get_pos() + obj_shift);
518       }
519     }
520   }
521 }
522 
523 void
change_solid_tiles(uint32_t old_tile_id,uint32_t new_tile_id)524 Sector::change_solid_tiles(uint32_t old_tile_id, uint32_t new_tile_id)
525 {
526   for (auto& solids: get_solid_tilemaps()) {
527     solids->change_all(old_tile_id, new_tile_id);
528   }
529 }
530 
531 void
set_gravity(float gravity)532 Sector::set_gravity(float gravity)
533 {
534   if (gravity != 10.0f)
535   {
536     log_warning << "Changing a Sector's gravitational constant might have unforeseen side-effects: " << gravity << std::endl;
537   }
538 
539   m_gravity = gravity;
540 }
541 
542 float
get_gravity() const543 Sector::get_gravity() const
544 {
545   return m_gravity;
546 }
547 
548 Player*
get_nearest_player(const Vector & pos) const549 Sector::get_nearest_player (const Vector& pos) const
550 {
551   Player *nearest_player = nullptr;
552   float nearest_dist = std::numeric_limits<float>::max();
553 
554   for (auto player_ptr : get_objects_by_type_index(typeid(Player)))
555   {
556     Player& player = *static_cast<Player*>(player_ptr);
557     if (player.is_dying() || player.is_dead())
558       continue;
559 
560     float dist = player.get_bbox ().distance(pos);
561 
562     if (dist < nearest_dist) {
563       nearest_player = &player;
564       nearest_dist = dist;
565     }
566   }
567 
568   return nearest_player;
569 }
570 
571 std::vector<MovingObject*>
get_nearby_objects(const Vector & center,float max_distance) const572 Sector::get_nearby_objects(const Vector& center, float max_distance) const
573 {
574   std::vector<MovingObject*> result;
575   for (auto& object : m_collision_system->get_nearby_objects(center, max_distance))
576   {
577     auto* moving_object = dynamic_cast<MovingObject*>(&object->get_listener());
578     if (moving_object) {
579       result.push_back(moving_object);
580     }
581   }
582   return result;
583 }
584 
585 void
stop_looping_sounds()586 Sector::stop_looping_sounds()
587 {
588   for (auto& object : get_objects()) {
589     object->stop_looping_sounds();
590   }
591 }
592 
play_looping_sounds()593 void Sector::play_looping_sounds()
594 {
595   for (const auto& object : get_objects()) {
596     object->play_looping_sounds();
597   }
598 }
599 
600 void
save(Writer & writer)601 Sector::save(Writer &writer)
602 {
603   BIND_SECTOR(*this);
604 
605   writer.start_list("sector", false);
606 
607   writer.write("name", m_name, false);
608 
609   if (!m_level.is_worldmap()) {
610     if (m_gravity != 10.0f) {
611       writer.write("gravity", m_gravity);
612     }
613   }
614 
615   if (m_init_script.size()) {
616     writer.write("init-script", m_init_script,false);
617   }
618 
619   // saving objects;
620   std::vector<GameObject*> objects(get_objects().size());
621   std::transform(get_objects().begin(), get_objects().end(), objects.begin(), [] (auto& obj) {
622     return obj.get();
623   });
624 
625   std::stable_sort(objects.begin(), objects.end(),
626                    [](const GameObject* lhs, GameObject* rhs) {
627                      return lhs->get_class() < rhs->get_class();
628                    });
629 
630   for (auto& obj : objects) {
631     if (obj->is_saveable()) {
632       writer.start_list(obj->get_class());
633       obj->save(writer);
634       writer.end_list(obj->get_class());
635     }
636   }
637 
638   writer.end_list("sector");
639 }
640 
641 void
convert_tiles2gameobject()642 Sector::convert_tiles2gameobject()
643 {
644   // add lights for special tiles
645   for (auto& tm : get_objects_by_type<TileMap>())
646   {
647     // Since object setup is not yet complete, I have to manually add the offset
648     // See https://github.com/SuperTux/supertux/issues/1378 for details
649     Vector tm_offset = tm.get_path() ? tm.get_path()->get_base() : Vector(0, 0);
650 
651     for (int x=0; x < tm.get_width(); ++x)
652     {
653       for (int y=0; y < tm.get_height(); ++y)
654       {
655         const Tile& tile = tm.get_tile(x, y);
656 
657         if (!tile.get_object_name().empty())
658         {
659           // If a tile is associated with an object, insert that
660           // object and remove the tile
661           if (tile.get_object_name() == "decal" ||
662               tm.is_solid())
663           {
664             Vector pos = tm.get_tile_position(x, y) + tm_offset;
665             try {
666               auto object = GameObjectFactory::instance().create(tile.get_object_name(), pos, Direction::AUTO, tile.get_object_data());
667               add_object(std::move(object));
668               tm.change(x, y, 0);
669             } catch(std::exception& e) {
670               log_warning << e.what() << "" << std::endl;
671             }
672           }
673         }
674         else
675         {
676           // add lights for fire tiles
677           uint32_t attributes = tile.get_attributes();
678           Vector pos = tm.get_tile_position(x, y) + tm_offset;
679           Vector center = pos + Vector(16, 16);
680 
681           if (attributes & Tile::FIRE) {
682             if (attributes & Tile::HURTS) {
683               // lava or lavaflow
684               // space lights a bit
685               if ((tm.get_tile(x-1, y).get_attributes() != attributes || x%3 == 0)
686                   && (tm.get_tile(x, y-1).get_attributes() != attributes || y%3 == 0)) {
687                 float pseudo_rnd = static_cast<float>(static_cast<int>(pos.x) % 10) / 10;
688                 add<PulsingLight>(center, 1.0f + pseudo_rnd, 0.8f, 1.0f,
689                                   Color(1.0f, 0.3f, 0.0f, 1.0f));
690               }
691             } else {
692               // torch
693               float pseudo_rnd = static_cast<float>(static_cast<int>(pos.x) % 10) / 10;
694               add<PulsingLight>(center, 1.0f + pseudo_rnd, 0.9f, 1.0f,
695                                 Color(1.0f, 1.0f, 0.6f, 1.0f));
696             }
697           }
698         }
699       }
700     }
701   }
702 }
703 
704 void
run_script(const std::string & script,const std::string & sourcename)705 Sector::run_script(const std::string& script, const std::string& sourcename)
706 {
707   m_squirrel_environment->run_script(script, sourcename);
708 }
709 
710 Camera&
get_camera() const711 Sector::get_camera() const
712 {
713   return get_singleton_by_type<Camera>();
714 }
715 
716 Player&
get_player() const717 Sector::get_player() const
718 {
719   return *static_cast<Player*>(get_objects_by_type_index(typeid(Player)).at(0));
720 }
721 
722 DisplayEffect&
get_effect() const723 Sector::get_effect() const
724 {
725   return get_singleton_by_type<DisplayEffect>();
726 }
727 
728 /* EOF */
729