1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-09-16 09:16:22 +02:00

Add Market chest and Train station teleport (#1478)

This commit is contained in:
RedRafe
2025-01-04 10:45:21 +01:00
committed by GitHub
parent 1b75ab4322
commit 772845ffa9
13 changed files with 621 additions and 7 deletions

View File

@@ -216,7 +216,10 @@ storage.config = {
{name = 'coin', count = 20000},
{name = 'infinity-pipe', count = 10},
{name = 'heat-interface', count = 10},
{name = 'selection-tool', count = 1}
{name = 'selection-tool', count = 1},
{name = 'linked-chest', count = 10},
{name = 'train-stop', count = 10},
{name = 'rail', count = 100},
}
}
},
@@ -469,6 +472,10 @@ storage.config = {
battery_charge = true,
}
},
train_station_teleport = {
enabled = false,
radius = 13,
},
admin_panel = {
enabled = true,
},
@@ -572,6 +579,26 @@ storage.config = {
override_sound_type = 'ambient' -- Menu > Settings > Sounds > Music
}
},
market_chest = {
enabled = false,
market_provides_chests = true,
-- What market provides
offers = {
['coal'] = 2,
['copper-ore'] = 2,
['iron-ore'] = 2,
['stone'] = 2,
['uranium-ore'] = 10,
},
-- What market requests
requests = {
['coal'] = 1,
['copper-ore'] = 1,
['iron-ore'] = 1,
['stone'] = 1,
['uranium-ore'] = 5,
},
}
}
return storage.config

View File

@@ -176,6 +176,12 @@ end
if config.player_shortcuts.enabled then
require 'features.gui.shortcuts'
end
if config.train_station_teleport.enabled then
require 'features.train_station_teleport'
end
if config.market_chest.enabled then
require 'features.market_chest'
end
if config.experience.enabled then
require 'features.gui.experience'
end

311
features/market_chest.lua Normal file
View File

@@ -0,0 +1,311 @@
-- This feature replaces placed linked-chests with buffer chests that can automatically trade items
local Buckets = require 'utils.buckets'
local Event = require 'utils.event'
local Global = require 'utils.global'
local Gui = require 'utils.gui'
local Retailer = require 'features.retailer'
local config = require 'config'.market_chest
local floor = math.floor
local b_get = Buckets.get
local b_add = Buckets.add
local b_remove = Buckets.remove
local b_bucket = Buckets.get_bucket
local register_on_object_destroyed = script.register_on_object_destroyed
local relative_frame_name = Gui.uid_name()
local offer_tag_name = Gui.uid_name()
local request_tag_name = Gui.uid_name()
local standard_market_name = 'fish_market'
-- What market provides
local DEFAULT_OFFERS = {
['coal'] = 2,
['copper-ore'] = 2,
['iron-ore'] = 2,
['stone'] = 2,
['uranium-ore'] = 10,
}
-- What market requests
local DEFAULT_REQUESTS = {
['coal'] = 1,
['copper-ore'] = 1,
['iron-ore'] = 1,
['stone'] = 1,
['uranium-ore'] = 5,
}
local this = {
chests = Buckets.new(),
enabled = config.enabled or false,
offers = config.offers or DEFAULT_OFFERS,
requests = config.requests or DEFAULT_REQUESTS,
relative_gui = {},
}
Global.register(this, function(tbl) this = tbl end)
local function update_entity(entity)
if not (entity and entity.valid) then
return
end
local data = b_get(this.chests, entity.unit_number)
local offer, request, ratio = data.offer, data.request, data.ratio
if not offer or not request or not ratio then
return
end
local inv = entity.get_inventory(defines.inventory.chest)
if not (inv and inv.valid) then
return
end
local r_count = inv.get_item_count(request)
local o_count = floor(r_count * ratio)
if o_count == 0 or r_count == 0 then
return
end
local removed = inv.remove {
name = request,
quality = request.quality,
count = o_count / ratio,
}
if removed > 0 then
local inserted = inv.insert {
name = offer,
count = o_count,
}
if inserted < o_count then
inv.insert {
name = request,
count = floor((o_count - inserted) / ratio),
}
end
end
end
local function update_market(enabled, price)
if enabled then
if not price then
local chest = Retailer.get_items(standard_market_name)['linked-chest']
price = chest and chest.price or 3000
end
Retailer.set_item(standard_market_name, { name = 'linked-chest', price = price })
else
Retailer.remove_item(standard_market_name, 'linked-chest')
end
end
Event.on_built(function(event)
local entity = event.entity
if not (entity and entity.valid and entity.name == 'linked-chest') then
return
end
-- Replace with buffer chest
local force = entity.force
local position = entity.position
local surface = entity.surface
entity.destroy()
local chest = surface.create_entity{
name = 'buffer-chest',
position = position,
force = force,
}
chest.destructible = false
b_add(this.chests, chest.unit_number, { entity = chest })
register_on_object_destroyed(chest)
update_entity(chest)
rendering.draw_sprite {
sprite = 'entity.market',
surface = chest.surface,
only_in_alt_mode = true,
target = {
entity = chest,
offset = { 0, 0 },
},
}
end)
Event.on_destroyed(function(event)
local id = event.useful_id or event.entity.unit_number
local data = b_get(this.chests, id)
local inv = event.buffer
if data and inv and inv.valid and inv.get_item_count { name = 'buffer-chest' } > 0 then
update_entity(data.entity)
b_remove(this.chests, id)
inv.remove { name = 'buffer-chest', count = 1 }
inv.insert { name = 'linked-chest', count = 1 }
end
end)
Event.add(defines.events.on_tick, function(event)
if not this.enabled then
return
end
for unit_number, data in pairs(b_bucket(this.chests, event.tick)) do
local entity = data.entity
if entity.valid then
update_entity(entity)
else
b_remove(this.chests, unit_number)
end
end
end)
Event.add(defines.events.on_gui_opened, function(event)
if event.gui_type ~= defines.gui_type.entity then
return
end
local old = this.relative_gui[event.player_index]
if old and old.valid then
Gui.destroy(old)
end
if not this.enabled then
return
end
local entity = event.entity
if not entity or entity.name ~= 'buffer-chest' then
return
end
local data = b_get(this.chests, entity.unit_number)
if not data then
return
end
local player = game.get_player(event.player_index)
local frame = player.gui.relative.add {
type = 'frame',
name = relative_frame_name,
direction = 'vertical',
anchor = {
gui = defines.relative_gui_type.container_gui,
position = defines.relative_gui_position.right,
}
}
Gui.set_style(frame, { horizontally_stretchable = false, padding = 3 })
local flow = frame.add { type = 'flow', direction = 'horizontal' }
flow.add { type = 'label', style = 'frame_title' }
local canvas = frame.add { type = 'frame', style = 'entity_frame', direction = 'vertical' }
local info = canvas.add { type = 'frame', style = 'deep_frame_in_shallow_frame_for_description', direction = 'vertical' }
info.add { type = 'label', caption = '[img=entity/market] Market chest', style = 'tooltip_heading_label_category' }
info.add { type = 'line', direction = 'horizontal', style = 'tooltip_category_line' }
local description = info.add { type = 'label', caption = {'market_chest.description'} }
Gui.set_style(description, { single_line = false, maximal_width = 184 })
local tables = {}
canvas.add { type = 'label', style = 'bold_label', caption = 'Requests [img=info]', tooltip = {'market_chest.requests_tooltip'} }
tables.requests = canvas
.add { type = 'frame', style = 'slot_button_deep_frame' }
.add { type = 'table', style = 'filter_slot_table', column_count = 5 }
for name, value in pairs(this.requests) do
local button = tables.requests.add {
type = 'sprite-button',
sprite = 'item/'..name,
number = value,
tooltip = {'market_chest.item_tooltip', value},
tags = { name = request_tag_name, item = name, id = entity.unit_number },
toggled = data.request and data.request == name,
}
Gui.set_data(button, tables)
end
canvas.add { type = 'line', direction = 'horizontal' }
canvas.add { type = 'label', style = 'bold_label', caption = 'Offers [img=info]', tooltip = {'market_chest.offers_tooltip'} }
tables.offers = canvas
.add { type = 'frame', style = 'slot_button_deep_frame' }
.add { type = 'table', style = 'filter_slot_table', column_count = 5 }
for name, value in pairs(this.offers) do
local button = tables.offers.add {
type = 'sprite-button',
sprite = 'item/'..name,
number = value,
tooltip = {'market_chest.item_tooltip', value},
tags = { name = offer_tag_name, item = name, id = entity.unit_number },
toggled = data.offer and data.offer == name,
}
Gui.set_data(button, tables)
end
this.relative_gui[event.player_index] = frame
end)
Event.add(defines.events.on_gui_click, function(event)
local element = event.element
if not (element and element.valid) then
return
end
local tag = element.tags and element.tags.name
if not tag or not (tag == request_tag_name or tag == offer_tag_name) then
return
end
local toggled = not element.toggled
for _, button in pairs(element.parent.children) do
button.toggled = false
end
element.toggled = toggled
local item_name = element.tags.item
local data = b_get(this.chests, element.tags.id)
if tag == request_tag_name then
data.request = toggled and item_name or nil
elseif tag == offer_tag_name then
data.offer = toggled and item_name or nil
end
if data.request == data.offer then
data.request, data.offer = nil, nil
for _, t in pairs(Gui.get_data(element)) do
for _, button in pairs(t.children) do
button.toggled = false
end
end
end
if data.request and data.offer then
data.ratio = this.requests[data.request] / this.offers[data.offer]
else
data.ratio = nil
end
end)
Event.on_init(function()
update_market(config.market_provides_chests, 3000)
end)
local Public = {}
Public.get = function(key)
return this[key]
end
Public.set = function(key, value)
this[key] = value
end
Public.distribute_linked_chests = function(enabled, price)
update_market(enabled, price)
end
Public.spread = function(ticks)
Buckets.reallocate(this.chests, ticks)
end
return Public

View File

@@ -133,6 +133,7 @@ local item_worths = {
['passive-provider-chest'] = 256,
['requester-chest'] = 512,
['storage-chest'] = 256,
['linked-chest'] = 4096,
['logistic-robot'] = 256,
['logistic-science-pack'] = 16,
['long-handed-inserter'] = 16,
@@ -246,6 +247,18 @@ function Public.is_unlocked(name)
return item_unlocked[name] ~= nil
end
function Public.set_unlocked(name, unlocked, value)
if unlocked then
item_unlocked[name] = value or item_worths[name] or 64
if not table.contains(item_names, name) then
table_insert(item_names, name)
end
else
item_unlocked[name] = nil
table.remove_element(item_names, name)
end
end
local function get_raffle_keys()
local raffle_keys = {}
for i = 1, #item_names do
@@ -370,7 +383,6 @@ function Public.get_unlocked_item_values()
return item_unlocked
end
function Public.get_items_worth()
return item_worths
end

View File

@@ -0,0 +1,96 @@
-- This feature adds teleport shortcuts in train stop's GUI to allow players to teleport between tran stations.
-- A player must stand nearby a train station to be able to teleport, and must teleport to a physical train stop (not ghost).
local Event = require 'utils.event'
local Gui = require 'utils.gui'
local Global = require 'utils.global'
local config = require 'config'.train_station_teleport
local relative_frame_name = Gui.uid_name()
local teleport_button_name = Gui.uid_name()
local this = {
relative_gui = {},
radius = config.radius or 13,
enabled = config.enabled or false,
}
Global.register(this, function(tbl) this = tbl end)
Event.add(defines.events.on_gui_opened, function(event)
if event.gui_type ~= defines.gui_type.entity then
return
end
local old = this.relative_gui[event.player_index]
if old and old.valid then
Gui.destroy(old)
end
if not this.enabled then
return
end
local entity = event.entity
if not entity or entity.name ~= 'train-stop' then
return
end
local player = game.get_player(event.player_index)
local frame = player.gui.relative.add {
type = 'frame',
name = relative_frame_name,
direction = 'vertical',
anchor = {
gui = defines.relative_gui_type.train_stop_gui,
position = defines.relative_gui_position.top,
}
}
Gui.set_style(frame, { horizontally_stretchable = false, padding = 3 })
local canvas = frame.add { type = 'frame', style = 'inside_deep_frame', direction = 'vertical' }
Gui.set_style(canvas, { padding = 4 })
local button = canvas.add { type = 'button', name = teleport_button_name, caption = 'Teleport' , style = 'confirm_button_without_tooltip' }
Gui.set_data(button, { entity = entity })
this.relative_gui[event.player_index] = frame
end)
Gui.on_click(teleport_button_name, function(event)
local player = event.player
if player.physical_surface.count_entities_filtered({
position = player.physical_position,
radius = this.radius,
name = 'train-stop',
limit = 1,
}) == 0 then
player.print({ 'train_station_teleport.err_no_nearby_station' })
return
end
local entity = Gui.get_data(event.element).entity
if not (entity and entity.valid) then
return
end
local position = entity.surface.find_non_colliding_position('character', entity.position, this.radius, 0.2)
if position then
player.print({ 'train_station_teleport.success_destination', entity.backer_name })
player.teleport(position, entity.surface)
else
player.print({ 'train_station_teleport.err_no_valid_position' })
end
end)
local Public = {}
Public.get = function(key)
return this[key]
end
Public.set = function(key, value)
this[key] = value
end
return Public

View File

@@ -220,3 +220,14 @@ err_no_accumulators=[color=blue][Battery recharge][/color] No accumulators nearb
[clear_corpses]
count=[color=blue][Cleaner][/color] __1__ __plural_for_parameter__1__{1=corpse|rest=corpses}__ removed.
clear=[color=blue][Cleaner][/color] already clear.
[train_station_teleport]
err_no_nearby_station=[color=blue][Teleporter][/color] You must be near a train station before teleporting! Please move closer to a train station to initiate the teleport.
err_no_valid_position=[color=blue][Teleporter][/color] No valid position was found at the selected train station. Please ensure the destination is accessible and try again.
success_destination=[color=blue][Teleporter][/color] You have been teleported to [color=green]__1__[/color]! Enjoy your journey!
[market_chest]
description=Automatically trade materials with the Market from anywhere. The [font=default-semibold][color=128,205,240]Market[/color][/font] will constantly provide your selected [font=default-semibold][color=255,230,192]Offer[/color][/font] as long as the selected [font=default-semibold][color=255,230,192]Request[/color][/font] is provided to make the trade.\n\nThe exchange fee is computed based on the ratio of the worths of selected items.
requests_tooltip=Select an item that will be removed
offers_tooltip=Select an item that will be provided
item_tooltip=This item is worth __1__ [img=item/coin] __plural_for_parameter__1__{1=coin|rest=coins}__

View File

@@ -56,10 +56,17 @@ function Scenario.register(diggy_config)
redmew_config.reactor_meltdown.enabled = false
redmew_config.hodor.enabled = false
redmew_config.paint.enabled = false
redmew_config.player_shortcuts.enabled = true
redmew_config.train_station_teleport.enabled = true
redmew_config.experience.enabled = true
redmew_config.experience.sound.path = 'diggy-diggy-chorus'
redmew_config.experience.sound.duration = 5 * 60 * 60
redmew_config.player_shortcuts.enabled = true
table.insert(redmew_config.experience.unlockables, {
level = 120,
price = 3000,
name = 'linked-chest'
})
redmew_config.market_chest.enabled = true
restart_command({scenario_name = diggy_config.scenario_name})

View File

@@ -61,6 +61,10 @@ function Market.spawn_exchange_market(position)
local max_attempts = 10
local most_expensive_item = { value = 0 }
if storage.config.market_chest.enabled then
PriceRaffle.set_unlocked('linked-chest', true)
end
local unlocked_items = PriceRaffle.get_unlocked_item_names()
for _ = 1, offers_count do
local inserted = false
@@ -77,7 +81,7 @@ function Market.spawn_exchange_market(position)
if price / stack_size < 80 then
market.add_market_item {
offer = { type = 'give-item', item = expensive, count = 1 },
price = {{ name = cheap, type = 'item', amount = price }},
price = {{ name = cheap, type = 'item', count = price }},
}
if expensive_value > most_expensive_item.value then
most_expensive_item.name = expensive
@@ -101,7 +105,7 @@ function Market.spawn_exchange_market(position)
if price / stack_size < 50 then
market.add_market_item {
offer = { type = 'give-item', item = expensive, count = 1 },
price = {{ name = cheap, type = 'item', amount = price }},
price = {{ name = cheap, type = 'item', count = price }},
}
if expensive_value > most_expensive_item.value then
most_expensive_item.name = expensive

View File

@@ -72,6 +72,8 @@ Config.market.enabled = false
Config.player_rewards.enabled = false
Config.player_shortcuts.enabled = true
Config.dump_offline_inventories.enabled = true
Config.market_chest.enabled = true
Config.train_station_teleport.enabled = true
Config.player_create.starting_items = {
{ name = 'burner-mining-drill', count = 1 },
{ name = 'stone-furnace', count = 1 },
@@ -80,6 +82,7 @@ Config.player_create.starting_items = {
{ name = 'wood', count = 1 },
}
if script.active_mods['Krastorio2'] then
Config.paint.enabled = false
storage.config.redmew_qol.loaders = false

View File

@@ -36,4 +36,12 @@ Public.default_pusher = {
}
}
Public.default_dragger = {
style = {
vertically_stretchable = true,
horizontally_stretchable = true,
margin = 0
}
}
return Public

83
utils/buckets.lua Normal file
View File

@@ -0,0 +1,83 @@
local DEFAULT_INTERVAL = 60
local Buckets = {}
---@param interval? number
function Buckets.new(interval)
return { list = {}, interval = interval or DEFAULT_INTERVAL }
end
---@param bucket table
---@param id number|string
---@param data any
function Buckets.add(bucket, id, data)
local bucket_id = id % bucket.interval
bucket.list[bucket_id] = bucket.list[bucket_id] or {}
bucket.list[bucket_id][id] = data or {}
end
---@param bucket table
---@param id number|string
function Buckets.get(bucket, id)
if not id then return end
local bucket_id = id % bucket.interval
return bucket.list[bucket_id] and bucket.list[bucket_id][id]
end
---@param bucket table
---@param id number|string
function Buckets.remove(bucket, id)
if not id then return end
local bucket_id = id % bucket.interval
if bucket.list[bucket_id] then
bucket.list[bucket_id][id] = nil
end
end
---@param bucket table
---@param id number|string
function Buckets.get_bucket(bucket, id)
local bucket_id = id % bucket.interval
bucket.list[bucket_id] = bucket.list[bucket_id] or {}
return bucket.list[bucket_id]
end
-- Redistributes current buckets content over a new time interval
---@param bucket table
---@param new_interval number
function Buckets.reallocate(bucket, new_interval)
new_interval = new_interval or DEFAULT_INTERVAL
if bucket.interval == new_interval then
return
end
local tmp = {}
-- Collect data from existing buckets
for b_id = 0, bucket.interval - 1 do
for id, data in pairs(bucket.list[b_id] or {}) do
tmp[id] = data
end
end
-- Clear old buckets
bucket.list = {}
-- Update interval and reinsert data
bucket.interval = new_interval
for id, data in pairs(tmp) do
Buckets.add(bucket, id, data)
end
end
-- Distributes a table's content over a time interval
---@param tbl table
---@param interval? number
function Buckets.migrate(tbl, interval)
local bucket = Buckets.new(interval)
for id, data in pairs(tbl) do
Buckets.add(bucket, id, data)
end
return bucket
end
return Buckets

View File

@@ -112,7 +112,7 @@ local generate_event_name = script.generate_event_name
local Event = {}
local handlers_added = false -- set to true after the removeable event handlers have been added.
local handlers_added = false -- set to true after the removable event handlers have been added.
local event_handlers = EventCore.get_event_handlers()
local on_nth_tick_event_handlers = EventCore.get_on_nth_tick_event_handlers()
@@ -142,7 +142,7 @@ local function remove(tbl, handler)
return
end
-- the handler we are looking for is more likly to be at the back of the array.
-- the handler we are looking for is more likely to be at the back of the array.
for i = #tbl, 1, -1 do
if tbl[i] == handler then
table_remove(tbl, i)
@@ -213,6 +213,41 @@ function Event.on_nth_tick(tick, handler)
core_on_nth_tick(tick, handler)
end
local function handler_factory(event_list)
return function(handler)
for _, event_name in pairs(event_list) do
Event.add(event_name, handler)
end
end
end
Event.on_built = handler_factory {
defines.events.on_biter_base_built,
defines.events.on_built_entity,
defines.events.on_robot_built_entity,
defines.events.on_space_platform_built_entity,
defines.events.script_raised_built,
defines.events.script_raised_revive,
defines.events.on_entity_cloned,
}
Event.on_destroyed = handler_factory {
defines.events.on_entity_died,
defines.events.on_player_mined_entity,
defines.events.on_robot_mined_entity,
defines.events.on_space_platform_mined_entity,
defines.events.script_raised_destroy,
}
Event.on_built_tile = handler_factory {
defines.events.on_player_built_tile,
defines.events.on_robot_built_tile,
defines.events.on_space_platform_built_tile,
}
Event.on_mined_tile = handler_factory {
defines.events.on_player_mined_tile,
defines.events.on_robot_mined_tile,
defines.events.on_space_platform_mined_tile,
}
--- Register a token handler that can be safely added and removed at runtime.
-- Do NOT call this method during on_load.
-- See documentation at top of file for details on using events.

View File

@@ -214,6 +214,17 @@ function Gui.add_pusher(parent, direction)
return pusher
end
Gui.add_dragger = function(parent, target)
local dragger = parent.add { type = 'empty-widget', style = 'draggable_space_header' }
Gui.set_style(dragger, Styles.default_dragger.style)
if target then
dragger.drag_target = target
end
return dragger
end
local function handler_factory(event_id)
local handlers