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