Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2025-03-23 12:13:25 +1100
committerGitHub <noreply@github.com>2025-03-23 12:13:25 +1100
commit1c286ade1fdf4d274cb2c6100a023f2396847786 (patch)
tree1cf8f98e83324da3cdaa4cd3065ce212fa9d7794
parent3f761123e939ece5c8805c165dc625756b950f8b (diff)
parentd23db8d5515cd8b5b1bb59b19c4285053eff5ee7 (diff)
Merge pull request #215 from chililisoup/main
Block swapping
-rw-r--r--assets/opensb/binds/opensb.binds5
-rw-r--r--assets/opensb/interface/building/blockswap.pngbin0 -> 147 bytes
-rw-r--r--source/game/StarNetPackets.cpp20
-rw-r--r--source/game/StarNetPackets.hpp16
-rw-r--r--source/game/StarPlayer.cpp4
-rw-r--r--source/game/StarPlayer.hpp1
-rw-r--r--source/game/StarWorldClient.cpp27
-rw-r--r--source/game/StarWorldClient.hpp2
-rw-r--r--source/game/StarWorldImpl.hpp50
-rw-r--r--source/game/StarWorldServer.cpp102
-rw-r--r--source/game/StarWorldServer.hpp7
-rw-r--r--source/game/interfaces/StarWorld.hpp5
-rw-r--r--source/game/items/StarMaterialItem.cpp118
-rw-r--r--source/game/items/StarMaterialItem.hpp2
14 files changed, 325 insertions, 34 deletions
diff --git a/assets/opensb/binds/opensb.binds b/assets/opensb/binds/opensb.binds
index d06b6f4..381bbc9 100644
--- a/assets/opensb/binds/opensb.binds
+++ b/assets/opensb/binds/opensb.binds
@@ -50,6 +50,11 @@
"group": "building",
"name": "Shrink Building Radius"
},
+ "blockSwapToggle": {
+ "default": [],
+ "group": "building",
+ "name": "Toggle Block Swap"
+ },
"editingCopyItemJson": {
"default": [], // [{"type": "key", "value": "C","mods": ["LShift"]}],
"name": "Copy Item in Cursor",
diff --git a/assets/opensb/interface/building/blockswap.png b/assets/opensb/interface/building/blockswap.png
new file mode 100644
index 0000000..83f49b4
--- /dev/null
+++ b/assets/opensb/interface/building/blockswap.png
Binary files differ
diff --git a/source/game/StarNetPackets.cpp b/source/game/StarNetPackets.cpp
index 2e64eee..dfe3ff1 100644
--- a/source/game/StarNetPackets.cpp
+++ b/source/game/StarNetPackets.cpp
@@ -75,7 +75,9 @@ EnumMap<PacketType> const PacketTypeNames{
{PacketType::SystemObjectDestroy, "SystemObjectDestroy"},
{PacketType::SystemShipCreate, "SystemShipCreate"},
{PacketType::SystemShipDestroy, "SystemShipDestroy"},
- {PacketType::SystemObjectSpawn, "SystemObjectSpawn"}
+ {PacketType::SystemObjectSpawn, "SystemObjectSpawn"},
+ // OpenStarbound packets
+ {PacketType::ReplaceTileList, "ReplaceTileList"}
};
EnumMap<NetCompressionMode> const NetCompressionModeNames {
@@ -137,6 +139,7 @@ PacketPtr createPacket(PacketType type) {
case PacketType::FindUniqueEntityResponse: return make_shared<FindUniqueEntityResponsePacket>();
case PacketType::Pong: return make_shared<PongPacket>();
case PacketType::ModifyTileList: return make_shared<ModifyTileListPacket>();
+ case PacketType::ReplaceTileList: return make_shared<ReplaceTileListPacket>();
case PacketType::DamageTileGroup: return make_shared<DamageTileGroupPacket>();
case PacketType::CollectLiquid: return make_shared<CollectLiquidPacket>();
case PacketType::RequestDrop: return make_shared<RequestDropPacket>();
@@ -747,6 +750,21 @@ void ModifyTileListPacket::write(DataStream& ds) const {
ds.write(allowEntityOverlap);
}
+ReplaceTileListPacket::ReplaceTileListPacket() {}
+
+ReplaceTileListPacket::ReplaceTileListPacket(TileModificationList modifications, TileDamage tileDamage)
+ : modifications(modifications), tileDamage(std::move(tileDamage)) {}
+
+void ReplaceTileListPacket::read(DataStream& ds) {
+ ds.readContainer(modifications);
+ ds.read(tileDamage);
+}
+
+void ReplaceTileListPacket::write(DataStream& ds) const {
+ ds.writeContainer(modifications);
+ ds.write(tileDamage);
+}
+
DamageTileGroupPacket::DamageTileGroupPacket() : layer(TileLayer::Foreground) {}
DamageTileGroupPacket::DamageTileGroupPacket(
diff --git a/source/game/StarNetPackets.hpp b/source/game/StarNetPackets.hpp
index 4002e90..349191f 100644
--- a/source/game/StarNetPackets.hpp
+++ b/source/game/StarNetPackets.hpp
@@ -113,7 +113,10 @@ enum class PacketType : uint8_t {
SystemShipDestroy,
// Packets sent system client -> system server
- SystemObjectSpawn
+ SystemObjectSpawn,
+
+ // OpenStarbound packets
+ ReplaceTileList
};
extern EnumMap<PacketType> const PacketTypeNames;
@@ -627,6 +630,17 @@ struct ModifyTileListPacket : PacketBase<PacketType::ModifyTileList> {
bool allowEntityOverlap;
};
+struct ReplaceTileListPacket : PacketBase<PacketType::ReplaceTileList> {
+ ReplaceTileListPacket();
+ ReplaceTileListPacket(TileModificationList modifications, TileDamage tileDamage);
+
+ void read(DataStream& ds) override;
+ void write(DataStream& ds) const override;
+
+ TileModificationList modifications;
+ TileDamage tileDamage;
+};
+
struct DamageTileGroupPacket : PacketBase<PacketType::DamageTileGroup> {
DamageTileGroupPacket();
DamageTileGroupPacket(List<Vec2I> tilePositions, TileLayer layer, Vec2F sourcePosition, TileDamage tileDamage, Maybe<EntityId> sourceEntity);
diff --git a/source/game/StarPlayer.cpp b/source/game/StarPlayer.cpp
index af85cce..43add67 100644
--- a/source/game/StarPlayer.cpp
+++ b/source/game/StarPlayer.cpp
@@ -1273,6 +1273,10 @@ void Player::triggerPickupEvents(ItemPtr const& item) {
}
}
+ItemPtr Player::essentialItem(EssentialItem essentialItem) const {
+ return m_inventory->essentialItem(essentialItem);
+}
+
bool Player::hasItem(ItemDescriptor const& descriptor, bool exactMatch) const {
return m_inventory->hasItem(descriptor, exactMatch);
}
diff --git a/source/game/StarPlayer.hpp b/source/game/StarPlayer.hpp
index 1a6a5c2..5cc6f8f 100644
--- a/source/game/StarPlayer.hpp
+++ b/source/game/StarPlayer.hpp
@@ -223,6 +223,7 @@ public:
void triggerPickupEvents(ItemPtr const& item);
+ ItemPtr essentialItem(EssentialItem essentialItem) const;
bool hasItem(ItemDescriptor const& descriptor, bool exactMatch = false) const;
uint64_t hasCountOfItem(ItemDescriptor const& descriptor, bool exactMatch = false) const;
// altough multiple entries may match, they might have different
diff --git a/source/game/StarWorldClient.cpp b/source/game/StarWorldClient.cpp
index 82c09ae..d4b8151 100644
--- a/source/game/StarWorldClient.cpp
+++ b/source/game/StarWorldClient.cpp
@@ -375,6 +375,33 @@ TileModificationList WorldClient::applyTileModifications(TileModificationList co
return failures;
}
+TileModificationList WorldClient::replaceTiles(TileModificationList const& modificationList, TileDamage const& tileDamage) {
+ if (!inWorld())
+ return {};
+
+ // Tell client it can't send a replace packet
+ if (m_clientState.netCompatibilityRules().isLegacy())
+ return modificationList;
+
+ TileModificationList success, failures;
+ for (auto pair : modificationList) {
+ if (!isTileProtected(pair.first) && WorldImpl::validateTileReplacement(pair.second))
+ success.append(pair);
+ else
+ failures.append(pair);
+ }
+
+ m_outgoingPackets.append(make_shared<ReplaceTileListPacket>(std::move(success), tileDamage));
+
+ return failures;
+}
+
+bool WorldClient::damageWouldDestroy(Vec2I const& pos, TileLayer layer, TileDamage const& tileDamage) const {
+ if (!inWorld())
+ return false;
+ return WorldImpl::damageWouldDestroy(m_tileArray, pos, layer, tileDamage);
+}
+
float WorldClient::gravity(Vec2F const& pos) const {
if (!inWorld())
return 0.0f;
diff --git a/source/game/StarWorldClient.hpp b/source/game/StarWorldClient.hpp
index 3c8728d..195a95c 100644
--- a/source/game/StarWorldClient.hpp
+++ b/source/game/StarWorldClient.hpp
@@ -53,6 +53,8 @@ public:
LiquidLevel liquidLevel(RectF const& region) const override;
TileModificationList validTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) const override;
TileModificationList applyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) override;
+ TileModificationList replaceTiles(TileModificationList const& modificationList, TileDamage const& tileDamage) override;
+ bool damageWouldDestroy(Vec2I const& pos, TileLayer layer, TileDamage const& tileDamage) const override;
EntityPtr entity(EntityId entityId) const override;
void addEntity(EntityPtr const& entity, EntityId entityId = NullEntityId) override;
EntityPtr closestEntity(Vec2F const& center, float radius, EntityFilter selector = EntityFilter()) const override;
diff --git a/source/game/StarWorldImpl.hpp b/source/game/StarWorldImpl.hpp
index a567ae6..ee3ef11 100644
--- a/source/game/StarWorldImpl.hpp
+++ b/source/game/StarWorldImpl.hpp
@@ -39,6 +39,10 @@ namespace WorldImpl {
List<Vec2I> collidingTilesAlongLine(WorldGeometry const& worldGeometry, shared_ptr<TileSectorArray> const& tileSectorArray,
Vec2F const& begin, Vec2F const& end, CollisionSet const& collisionSet, size_t maxSize, bool includeEdges);
+ inline TileDamageParameters tileDamageParameters(WorldTile* tile, TileLayer layer, TileDamage const& tileDamage);
+ template <typename TileSectorArray>
+ bool damageWouldDestroy(shared_ptr<TileSectorArray> const& tileSectorArray, Vec2I pos, TileLayer layer, TileDamage const& tileDamage);
+
template <typename GetTileFunction>
bool canPlaceMaterial(EntityMapPtr const& entityMap,
Vec2I const& pos, TileLayer layer, MaterialId material, bool allowEntityOverlap, bool allowTileOverlap, GetTileFunction& getTile);
@@ -54,6 +58,7 @@ namespace WorldImpl {
bool canPlaceMod(Vec2I const& pos, TileLayer layer, ModId mod, GetTileFunction& getTile);
template <typename GetTileFunction>
pair<bool, bool> validateTileModification(EntityMapPtr const& entityMap, Vec2I const& pos, TileModification const& modification, bool allowEntityOverlap, GetTileFunction& getTile);
+ bool validateTileReplacement(TileModification const& modification);
// Split modification list into good and bad
template <typename GetTileFunction>
pair<TileModificationList, TileModificationList> splitTileModifications(EntityMapPtr const& entityMap, TileModificationList const& modificationList,
@@ -218,6 +223,36 @@ namespace WorldImpl {
return res;
}
+ inline TileDamageParameters tileDamageParameters(WorldTile* tile, TileLayer layer, TileDamage const& tileDamage) {
+ bool foreground = layer == TileLayer::Foreground;
+ auto materialDatabase = Root::singleton().materialDatabase();
+ auto target = foreground ? tile->foreground : tile->background;
+ auto mod = foreground ? tile->foregroundMod : tile->backgroundMod;
+
+ if (!isRealMod(mod))
+ return materialDatabase->materialDamageParameters(target);
+
+ if (tileDamageIsPenetrating(tileDamage.type))
+ return materialDatabase->materialDamageParameters(target);
+
+ if (materialDatabase->modBreaksWithTile(mod))
+ return materialDatabase->modDamageParameters(mod).sum(materialDatabase->materialDamageParameters(target));
+
+ return materialDatabase->modDamageParameters(mod);
+ }
+
+ template <typename TileSectorArray>
+ bool damageWouldDestroy(shared_ptr<TileSectorArray> const& tileSectorArray, Vec2I pos, TileLayer layer, TileDamage const& tileDamage) {
+ if (auto tile = tileSectorArray->modifyTile(pos)) {
+ auto damageParameters = tileDamageParameters(tile, layer, tileDamage);
+ float percentageDelta = damageParameters.damageDone(tileDamage) / damageParameters.totalHealth();
+ auto damage = layer == TileLayer::Foreground ? tile->foregroundDamage : tile->backgroundDamage;
+ return percentageDelta + damage.damagePercentage() >= 1.0f;
+ }
+
+ return false;
+ }
+
template <typename GetTileFunction>
bool canPlaceMaterial(EntityMapPtr const& entityMap,
Vec2I const& pos, TileLayer layer, MaterialId material, bool allowEntityOverlap, bool allowTileOverlap, GetTileFunction& getTile) {
@@ -349,6 +384,21 @@ namespace WorldImpl {
return { good, perhaps };
}
+ inline bool validateTileReplacement(TileModification const& modification) {
+ if (auto placeMaterial = modification.ptr<PlaceMaterial>()) {
+ if (!isRealMaterial(placeMaterial->material))
+ return false;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+ if (!materialDatabase->canPlaceInLayer(placeMaterial->material, placeMaterial->layer))
+ return false;
+
+ return true;
+ }
+
+ return false;
+ }
+
template <typename GetTileFunction>
pair<TileModificationList, TileModificationList> splitTileModifications(EntityMapPtr const& entityMap, TileModificationList const& modificationList,
bool allowEntityOverlap, GetTileFunction& getTile, function<bool(Vec2I pos, TileModification modification)> extraCheck) {
diff --git a/source/game/StarWorldServer.cpp b/source/game/StarWorldServer.cpp
index 0c2a089..9d53d54 100644
--- a/source/game/StarWorldServer.cpp
+++ b/source/game/StarWorldServer.cpp
@@ -373,6 +373,11 @@ void WorldServer::handleIncomingPackets(ConnectionId clientId, List<PacketPtr> c
if (!unappliedModifications.empty())
clientInfo->outgoingPackets.append(make_shared<TileModificationFailurePacket>(unappliedModifications));
+ } else if (auto rtpacket = as<ReplaceTileListPacket>(packet)) {
+ auto unappliedModifications = replaceTiles(rtpacket->modifications, rtpacket->tileDamage);
+ if (!unappliedModifications.empty())
+ clientInfo->outgoingPackets.append(make_shared<TileModificationFailurePacket>(unappliedModifications));
+
} else if (auto dtgpacket = as<DamageTileGroupPacket>(packet)) {
damageTiles(dtgpacket->tilePositions, dtgpacket->layer, dtgpacket->sourcePosition, dtgpacket->tileDamage, dtgpacket->sourceEntity);
@@ -875,6 +880,58 @@ TileModificationList WorldServer::forceApplyTileModifications(TileModificationLi
return doApplyTileModifications(modificationList, allowEntityOverlap, true);
}
+bool WorldServer::replaceTile(Vec2I const& pos, TileModification const& modification, TileDamage const& tileDamage) {
+ if (isTileProtected(pos))
+ return false;
+
+ if (!WorldImpl::validateTileReplacement(modification))
+ return false;
+
+ if (auto placeMaterial = modification.ptr<PlaceMaterial>()) {
+ if (!isTileConnectable(pos, placeMaterial->layer, true))
+ return false;
+
+ if (auto tile = m_tileArray->modifyTile(pos)) {
+ auto damageParameters = WorldImpl::tileDamageParameters(tile, placeMaterial->layer, tileDamage);
+ bool harvested = tileDamage.harvestLevel >= damageParameters.requiredHarvestLevel();
+ auto damage = placeMaterial->layer == TileLayer::Foreground ? tile->foregroundDamage : tile->backgroundDamage;
+ Vec2F dropPosition = centerOfTile(pos);
+
+ for (auto drop : destroyBlock(placeMaterial->layer, pos, harvested, !tileDamageIsPenetrating(damage.damageType()), false))
+ addEntity(ItemDrop::createRandomizedDrop(drop, dropPosition));
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+TileModificationList WorldServer::replaceTiles(TileModificationList const& modificationList, TileDamage const& tileDamage) {
+ TileModificationList success, failures;
+
+ for (auto pair : modificationList) {
+ if (replaceTile(pair.first, pair.second, tileDamage))
+ success.append(pair);
+ else
+ failures.append(pair);
+ }
+
+ failures.appendAll(doApplyTileModifications(success, true, false, false));
+
+ for (auto pair : success) {
+ checkEntityBreaks(RectF::withSize(Vec2F(pair.first), Vec2F(1, 1)));
+ m_liquidEngine->visitLocation(pair.first);
+ m_fallingBlocksAgent->visitLocation(pair.first);
+ }
+
+ return failures;
+}
+
+bool WorldServer::damageWouldDestroy(Vec2I const& pos, TileLayer layer, TileDamage const& tileDamage) const {
+ return WorldImpl::damageWouldDestroy(m_tileArray, pos, layer, tileDamage);
+}
+
TileDamageResult WorldServer::damageTiles(List<Vec2I> const& positions, TileLayer layer, Vec2F const& sourcePosition, TileDamage const& damage, Maybe<EntityId> sourceEntity) {
Set<Vec2I> positionSet;
for (auto const& pos : positions)
@@ -929,20 +986,11 @@ TileDamageResult WorldServer::damageTiles(List<Vec2I> const& positions, TileLaye
// Penetrating damage should carry through to the blocks behind this
// entity.
if (tileRes == TileDamageResult::None || tileDamageIsPenetrating(tileDamage.type)) {
- auto materialDatabase = Root::singleton().materialDatabase();
+ auto damageParameters = WorldImpl::tileDamageParameters(tile, layer, tileDamage);
if (layer == TileLayer::Foreground && isRealMaterial(tile->foreground)) {
if (!tile->rootSource) {
- if (isRealMod(tile->foregroundMod)) {
- if (tileDamageIsPenetrating(tileDamage.type))
- tile->foregroundDamage.damage(materialDatabase->materialDamageParameters(tile->foreground), sourcePosition, tileDamage);
- else if (materialDatabase->modBreaksWithTile(tile->foregroundMod))
- tile->foregroundDamage.damage(materialDatabase->modDamageParameters(tile->foregroundMod).sum(materialDatabase->materialDamageParameters(tile->foreground)), sourcePosition, tileDamage);
- else
- tile->foregroundDamage.damage(materialDatabase->modDamageParameters(tile->foregroundMod), sourcePosition, tileDamage);
- } else {
- tile->foregroundDamage.damage(materialDatabase->materialDamageParameters(tile->foreground), sourcePosition, tileDamage);
- }
+ tile->foregroundDamage.damage(damageParameters, sourcePosition, tileDamage);
// if the tile is broken, send a message back to the source entity with position, layer, dungeonId, and whether the tile was harvested
if (sourceEntity.isValid() && tile->foregroundDamage.dead()) {
@@ -964,16 +1012,7 @@ TileDamageResult WorldServer::damageTiles(List<Vec2I> const& positions, TileLaye
tileRes = TileDamageResult::Normal;
}
} else if (layer == TileLayer::Background && isRealMaterial(tile->background)) {
- if (isRealMod(tile->backgroundMod)) {
- if (tileDamageIsPenetrating(tileDamage.type))
- tile->backgroundDamage.damage(materialDatabase->materialDamageParameters(tile->background), sourcePosition, tileDamage);
- else if (materialDatabase->modBreaksWithTile(tile->backgroundMod))
- tile->backgroundDamage.damage(materialDatabase->modDamageParameters(tile->backgroundMod).sum(materialDatabase->materialDamageParameters(tile->background)), sourcePosition, tileDamage);
- else
- tile->backgroundDamage.damage(materialDatabase->modDamageParameters(tile->backgroundMod), sourcePosition, tileDamage);
- } else {
- tile->backgroundDamage.damage(materialDatabase->materialDamageParameters(tile->background), sourcePosition, tileDamage);
- }
+ tile->backgroundDamage.damage(damageParameters, sourcePosition, tileDamage);
// if the tile is broken, send a message back to the source entity with position and whether the tile was harvested
if (sourceEntity.isValid() && tile->backgroundDamage.dead()) {
@@ -1394,7 +1433,7 @@ Maybe<unsigned> WorldServer::shouldRunThisStep(String const& timingConfiguration
return {};
}
-TileModificationList WorldServer::doApplyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap, bool ignoreTileProtection) {
+TileModificationList WorldServer::doApplyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap, bool ignoreTileProtection, bool updateNeighbors) {
auto materialDatabase = Root::singleton().materialDatabase();
TileModificationList unapplied = modificationList;
@@ -1454,9 +1493,12 @@ TileModificationList WorldServer::doApplyTileModifications(TileModificationList
tile->dungeonId = ConstructionDungeonId;
- checkEntityBreaks(RectF::withSize(Vec2F(pos), Vec2F(1, 1)));
- m_liquidEngine->visitLocation(pos);
- m_fallingBlocksAgent->visitLocation(pos);
+ if (updateNeighbors) {
+ checkEntityBreaks(RectF::withSize(Vec2F(pos), Vec2F(1, 1)));
+ m_liquidEngine->visitLocation(pos);
+ m_fallingBlocksAgent->visitLocation(pos);
+ }
+
if (placeMaterial->layer == TileLayer::Foreground)
dirtyCollision(RectI::withSize(pos, {1, 1}));
queueTileUpdates(pos);
@@ -1737,7 +1779,7 @@ void WorldServer::setLiquid(Vec2I const& pos, LiquidId liquid, float level, floa
}
}
-List<ItemDescriptor> WorldServer::destroyBlock(TileLayer layer, Vec2I const& pos, bool genItems, bool destroyModFirst) {
+List<ItemDescriptor> WorldServer::destroyBlock(TileLayer layer, Vec2I const& pos, bool genItems, bool destroyModFirst, bool updateNeighbors) {
auto materialDatabase = Root::singleton().materialDatabase();
auto* tile = m_tileArray->modifyTile(pos);
@@ -1803,9 +1845,11 @@ List<ItemDescriptor> WorldServer::destroyBlock(TileLayer layer, Vec2I const& pos
tile->dungeonId = DestroyedBlockDungeonId;
- checkEntityBreaks(RectF::withSize(Vec2F(pos), Vec2F(1, 1)));
- m_liquidEngine->visitLocation(pos);
- m_fallingBlocksAgent->visitLocation(pos);
+ if (updateNeighbors) {
+ checkEntityBreaks(RectF::withSize(Vec2F(pos), Vec2F(1, 1)));
+ m_liquidEngine->visitLocation(pos);
+ m_fallingBlocksAgent->visitLocation(pos);
+ }
queueTileUpdates(pos);
queueTileDamageUpdates(pos, layer);
diff --git a/source/game/StarWorldServer.hpp b/source/game/StarWorldServer.hpp
index 63414f7..2e6d6a8 100644
--- a/source/game/StarWorldServer.hpp
+++ b/source/game/StarWorldServer.hpp
@@ -135,6 +135,9 @@ public:
TileModificationList validTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) const override;
TileModificationList applyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) override;
+ bool replaceTile(Vec2I const& pos, TileModification const& modification, TileDamage const& tileDamage);
+ TileModificationList replaceTiles(TileModificationList const& modificationList, TileDamage const& tileDamage) override;
+ bool damageWouldDestroy(Vec2I const& pos, TileLayer layer, TileDamage const& tileDamage) const override;
EntityPtr entity(EntityId entityId) const override;
void addEntity(EntityPtr const& entity, EntityId entityId = NullEntityId) override;
EntityPtr closestEntity(Vec2F const& center, float radius, EntityFilter selector = EntityFilter()) const override;
@@ -217,7 +220,7 @@ public:
SkyPtr sky() const;
void modifyLiquid(Vec2I const& pos, LiquidId liquid, float quantity, bool additive = false);
void setLiquid(Vec2I const& pos, LiquidId liquid, float level, float pressure);
- List<ItemDescriptor> destroyBlock(TileLayer layer, Vec2I const& pos, bool genItems, bool destroyModFirst);
+ List<ItemDescriptor> destroyBlock(TileLayer layer, Vec2I const& pos, bool genItems, bool destroyModFirst, bool updateNeighbors = true);
void removeEntity(EntityId entityId, bool andDie);
void updateTileEntityTiles(TileEntityPtr const& object, bool removing = false, bool checkBreaks = true);
@@ -314,7 +317,7 @@ private:
// of ticks since the last run.
Maybe<unsigned> shouldRunThisStep(String const& timingConfiguration);
- TileModificationList doApplyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap, bool ignoreTileProtection = false);
+ TileModificationList doApplyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap, bool ignoreTileProtection = false, bool updateNeighbors = true);
// Queues pending (step based) updates to the given player
void queueUpdatePackets(ConnectionId clientId, bool sendRemoteUpdates);
diff --git a/source/game/interfaces/StarWorld.hpp b/source/game/interfaces/StarWorld.hpp
index d718786..f2db119 100644
--- a/source/game/interfaces/StarWorld.hpp
+++ b/source/game/interfaces/StarWorld.hpp
@@ -46,6 +46,11 @@ public:
// Apply a list of tile modifications in the best order to apply as many
// possible, and returns the modifications that could not be applied.
virtual TileModificationList applyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) = 0;
+ // Swap existing tiles for ones defined in the modification list,
+ // and returns the modifications that could not be applied.
+ virtual TileModificationList replaceTiles(TileModificationList const& modificationList, TileDamage const& tileDamage) = 0;
+ // If an applied damage would destroy a tile
+ virtual bool damageWouldDestroy(Vec2I const& pos, TileLayer layer, TileDamage const& tileDamage) const = 0;
virtual bool isTileProtected(Vec2I const& pos) const = 0;
diff --git a/source/game/items/StarMaterialItem.cpp b/source/game/items/StarMaterialItem.cpp
index bd42bac..2f3aece 100644
--- a/source/game/items/StarMaterialItem.cpp
+++ b/source/game/items/StarMaterialItem.cpp
@@ -10,6 +10,7 @@
#include "StarInput.hpp"
#include "StarTileDrawer.hpp"
#include "StarPlayer.hpp"
+#include "StarTools.hpp"
namespace Star {
@@ -17,6 +18,7 @@ constexpr int BlockRadiusLimit = 16;
const String BlockRadiusPropertyKey = "building.blockRadius";
const String AltBlockRadiusPropertyKey = "building.altBlockRadius";
const String CollisionOverridePropertyKey = "building.collisionOverride";
+const String BlockSwapPropertyKey = "building.blockSwap";
MaterialItem::MaterialItem(Json const& config, String const& directory, Json const& settings)
: Item(config, directory, settings), FireableItem(config), BeamItem(config) {
@@ -42,6 +44,7 @@ MaterialItem::MaterialItem(Json const& config, String const& directory, Json con
m_blockRadius = config.getFloat("blockRadius", defaultParameters.getFloat("blockRadius"));
m_altBlockRadius = config.getFloat("altBlockRadius", defaultParameters.getFloat("altBlockRadius"));
m_collisionOverride = TileCollisionOverrideNames.maybeLeft(config.getString("collisionOverride", "None")).value(TileCollisionOverride::None);
+ m_blockSwap = false;
m_multiplace = config.getBool("allowMultiplace", BlockCollisionSet.contains(materialDatabase->materialCollisionKind(m_material)));
m_placeSounds = jsonToStringList(config.get("placeSounds", JsonArray()));
@@ -114,6 +117,13 @@ void MaterialItem::update(float dt, FireMode fireMode, bool shifting, HashSet<Mo
player->setSecretProperty(BlockRadiusPropertyKey, m_blockRadius);
owner()->addSound("/sfx/tools/buildradiusshrink.wav", 1.0f, 1.0f + m_blockRadius / BlockRadiusLimit);
}
+
+ if (auto presses = input.bindDown("opensb", "blockSwapToggle")) {
+ if (*presses % 2 != 0)
+ m_blockSwap = !m_blockSwap;
+ player->setSecretProperty(BlockSwapPropertyKey, m_blockSwap);
+ owner()->addSound(m_blockSwap ? "/sfx/interface/button/click.wav" : "/sfx/interface/button/release.wav", 1.0f, Random::randf(0.9f, 1.1f));
+ }
}
else
updatePropertiesFromPlayer(player);
@@ -121,7 +131,7 @@ void MaterialItem::update(float dt, FireMode fireMode, bool shifting, HashSet<Mo
}
void MaterialItem::render(RenderCallback* renderCallback, EntityRenderLayer) {
- if (m_collisionOverride != TileCollisionOverride::None) {
+ if (m_blockSwap || m_collisionOverride != TileCollisionOverride::None) {
float pulseLevel = 1.f - 0.3f * 0.5f * ((float)sin(2 * Constants::pi * 4.0 * Time::monotonicTime()) + 1.f);
Color color = owner()->favoriteColor().mix(Color::White);
color.setAlphaF(color.alphaF() * pulseLevel * 0.95f);
@@ -136,6 +146,12 @@ void MaterialItem::render(RenderCallback* renderCallback, EntityRenderLayer) {
}
};
+ if (m_blockSwap) {
+ color.hueShift(0.167f);
+ addIndicator("/interface/building/blockswap.png");
+ color.hueShift(-0.167f);
+ }
+
if (m_collisionOverride == TileCollisionOverride::Empty)
addIndicator("/interface/building/collisionempty.png");
else if (m_collisionOverride == TileCollisionOverride::Platform)
@@ -190,6 +206,9 @@ void MaterialItem::fire(FireMode mode, bool shifting, bool edgeTriggered) {
? collisionKindFromOverride(m_collisionOverride)
: Root::singleton().materialDatabase()->materialCollisionKind(m_material);
+ if (m_blockSwap && owner()->inToolRange(aimPosition))
+ blockSwap(radius, layer);
+
size_t total = 0;
for (unsigned i = 0; i != steps; ++i) {
auto placementOrigin = aimPosition + diff * (1.0f - (static_cast<float>(i) / steps));
@@ -223,6 +242,99 @@ void MaterialItem::endFire(FireMode, bool) {
m_lastAimPosition.reset();
}
+void MaterialItem::blockSwap(float radius, TileLayer layer) {
+ Player* player = as<Player>(owner());
+ if (!player)
+ return;
+
+ ItemPtr beamAxePtr = player->essentialItem(EssentialItem::BeamAxe);
+ if (!beamAxePtr)
+ return;
+
+ Item* beamAxe = beamAxePtr.get();
+ BeamMiningTool* tool = as<BeamMiningTool>(beamAxe);
+ if (!tool)
+ return;
+
+ List<Vec2I> swapPositions;
+ for (Vec2I& pos : tileArea(radius, owner()->aimPosition())) {
+ if (!world()->isTileConnectable(pos, layer, true))
+ continue;
+ if (world()->isTileProtected(pos))
+ continue;
+ if (world()->material(pos, layer) == materialId())
+ continue;
+ swapPositions.append(pos);
+ }
+
+ if (swapPositions.empty())
+ return;
+
+ auto materialDatabase = Root::singleton().materialDatabase();
+ auto assets = Root::singleton().assets();
+ String blockSound;
+
+ for (auto pos : swapPositions) {
+ blockSound = materialDatabase->miningSound(world()->material(pos, layer), world()->mod(pos, layer));
+ if (!blockSound.empty())
+ break;
+ }
+ if (blockSound.empty()) {
+ for (auto pos : swapPositions) {
+ blockSound = materialDatabase->footstepSound(world()->material(pos, layer), world()->mod(pos, layer));
+ if (!blockSound.empty()
+ && blockSound != assets->json("/client.config:defaultFootstepSound").toString())
+ break;
+ }
+ }
+
+ TileDamage damage;
+ damage.type = TileDamageType::Beamish;
+ damage.amount = beamAxe->instanceValue("tileDamage", 1.0f).toFloat();
+ damage.harvestLevel = beamAxe->instanceValue("harvestLevel", 1).toUInt();
+
+ TileModificationList toSwap;
+ List<Vec2I> toDamage;
+ for (auto pos : swapPositions) {
+ if (world()->damageWouldDestroy(pos, layer, damage))
+ toSwap.emplaceAppend(pos, PlaceMaterial{layer, materialId(), placementHueShift(pos), m_collisionOverride});
+ else
+ toDamage.append(pos);
+ }
+
+ if (toSwap.size() > count())
+ toSwap.resize(count());
+ if (toDamage.size() + toSwap.size() > count())
+ toDamage.resize(count() - toSwap.size());
+
+ if (!toSwap.empty()) {
+ size_t failed = world()->replaceTiles(toSwap, damage).size();
+
+ if (failed < toSwap.size())
+ consume(toSwap.size() - failed);
+ else {
+ for (auto pair : toSwap)
+ toDamage.append(pair.first);
+ if (toDamage.size() > count())
+ toDamage.resize(count());
+ }
+ }
+
+ if (!toDamage.empty()) {
+ auto damageResult = world()->damageTiles(toDamage, layer, owner()->position(), damage, owner()->entityId());
+ if (damageResult == TileDamageResult::Protected) {
+ blockSound = assets->json("/client.config:defaultDingSound").toString();
+ }
+ }
+
+ owner()->addSound(
+ Random::randValueFrom(jsonToStringList(beamAxe->instanceValue("strikeSounds"))),
+ assets->json("/sfx.config:miningToolVolume").toFloat()
+ );
+ owner()->addSound(blockSound, assets->json("/sfx.config:miningBlockVolume").toFloat());
+ setFireTimer(tool->windupTime() + tool->cooldownTime());
+}
+
MaterialId MaterialItem::materialId() const {
return m_material;
}
@@ -280,6 +392,10 @@ void MaterialItem::updatePropertiesFromPlayer(Player* player) {
auto collisionOverride = player->getSecretProperty(CollisionOverridePropertyKey);
if (collisionOverride.isType(Json::Type::String))
m_collisionOverride = TileCollisionOverrideNames.maybeLeft(collisionOverride.toString()).value(TileCollisionOverride::None);
+
+ auto blockSwap = player->getSecretProperty(BlockSwapPropertyKey);
+ if (blockSwap.isType(Json::Type::Bool))
+ m_blockSwap = blockSwap.toBool();
}
float MaterialItem::calcRadius(bool shifting) const {
diff --git a/source/game/items/StarMaterialItem.hpp b/source/game/items/StarMaterialItem.hpp
index 3bbb14c..cb423be 100644
--- a/source/game/items/StarMaterialItem.hpp
+++ b/source/game/items/StarMaterialItem.hpp
@@ -46,6 +46,7 @@ public:
List<PreviewTile> previewTiles(bool shifting) const override;
List<Drawable> const& generatedPreview(Vec2I position = {}) const;
private:
+ void blockSwap(float radius, TileLayer layer);
void updatePropertiesFromPlayer(Player* player);
float calcRadius(bool shifting) const;
List<Vec2I>& tileArea(float radius, Vec2F const& position) const;
@@ -56,6 +57,7 @@ private:
float m_blockRadius;
float m_altBlockRadius;
+ bool m_blockSwap;
bool m_shifting;
bool m_multiplace;
StringList m_placeSounds;