P16-01-DISMISS: 一般従業員の通常操作での解雇(退職金を含む。P16-01 の補助の作業。原単位への加算 0)
This commit is contained in:
+119
-29
@@ -6934,12 +6934,14 @@ func _rollback_staff_candidate_hire(snapshot: Dictionary) -> bool:
|
||||
|
||||
|
||||
## P16-01-DISMISS: whether a hired employee can be dismissed now, with the severance
|
||||
## (A14) and the Task the removal would reopen. Nothing is changed.
|
||||
## (A14), the Task the removal reopens and the rescue Tasks the command interrupts.
|
||||
## Nothing is changed.
|
||||
func get_staff_dismissal_preview(staff_id: StringName) -> Dictionary:
|
||||
var staff: StaffRuntime = staff_service.get_staff(staff_id) if staff_service != null else null
|
||||
var preview := {
|
||||
&"staff_id": staff_id, &"can_dismiss": false, &"reason": &"NOT_FOUND",
|
||||
&"severance": 0, &"unpaid_wages": 0, &"reopened_task_ids": [], &"reopened_task_types": [],
|
||||
&"interrupted_rescue_task_ids": [], &"interrupted_rescue_task_types": [], &"interrupted_rescue_roles": [],
|
||||
&"available_funds": FinancePolicy.available_funds(economy_service) if economy_service != null else 0,
|
||||
&"front_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT) if economy_service != null else 0,
|
||||
}
|
||||
@@ -6948,18 +6950,41 @@ func get_staff_dismissal_preview(staff_id: StringName) -> Dictionary:
|
||||
var severance := FinancePolicy.severance(staff.get_effective_daily_wage())
|
||||
preview[&"severance"] = severance
|
||||
preview[&"unpaid_wages"] = staff.unpaid_wages
|
||||
var task := staff_service.get_task(staff.current_task_id)
|
||||
if task != null and not task.is_terminal():
|
||||
preview[&"reopened_task_ids"] = [task.task_id]
|
||||
preview[&"reopened_task_types"] = [task.task_type]
|
||||
var reason := _staff_dismissal_block_reason(staff, severance)
|
||||
var work := _staff_dismissal_work(staff)
|
||||
for kind: StringName in [&"reopened", &"interrupted_rescue"]:
|
||||
for task: StaffTask in work[kind]:
|
||||
(preview[StringName(String(kind) + "_task_ids")] as Array).append(task.task_id)
|
||||
(preview[StringName(String(kind) + "_task_types")] as Array).append(task.task_type)
|
||||
for task: StaffTask in work[&"interrupted_rescue"]:
|
||||
(preview[&"interrupted_rescue_roles"] as Array).append(&"carrier" if task.task_id == staff.current_task_id else &"patient")
|
||||
# R5: after the game ends the command is refused before acceptance (submit_command).
|
||||
var ended := _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal()
|
||||
var reason: StringName = &"BANKRUPT" if ended else _staff_dismissal_block_reason(staff, severance)
|
||||
preview[&"reason"] = reason
|
||||
preview[&"can_dismiss"] = reason.is_empty()
|
||||
return preview
|
||||
|
||||
|
||||
## Every refusal of SPEC 6.3 except the Task Reservation release (R3), which only the
|
||||
## existing removal contract can decide and which then changes nothing.
|
||||
## The work a dismissal changes: the held Task the removal contract reopens, and the
|
||||
## rescue Tasks the command interrupts (the held transport and every rescue of the
|
||||
## employee as the patient; SPEC 6.2.1).
|
||||
func _staff_dismissal_work(staff: StaffRuntime) -> Dictionary:
|
||||
var reopened: Array[StaffTask] = []
|
||||
var interrupted: Array[StaffTask] = []
|
||||
var held := staff_service.get_task(staff.current_task_id)
|
||||
if held != null and not held.is_terminal():
|
||||
if held.task_type == &"rescue_transport":
|
||||
interrupted.append(held)
|
||||
else:
|
||||
reopened.append(held)
|
||||
for task: StaffTask in staff_service.get_tasks():
|
||||
if task.task_type in [&"rescue_pickup", &"rescue_transport"] and not task.is_terminal() \
|
||||
and task.target_entity_id == staff.staff_id and not interrupted.has(task):
|
||||
interrupted.append(task)
|
||||
return {&"reopened": reopened, &"interrupted_rescue": interrupted}
|
||||
|
||||
|
||||
## Every refusal of SPEC 6.3 the executor decides (R1, R2, R3, R7, R8, R9).
|
||||
func _staff_dismissal_block_reason(staff: StaffRuntime, severance: int) -> StringName:
|
||||
if staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE:
|
||||
return &"UNIQUE"
|
||||
@@ -6967,24 +6992,35 @@ func _staff_dismissal_block_reason(staff: StaffRuntime, severance: int) -> Strin
|
||||
return &"NOT_GENERAL_EMPLOYEE"
|
||||
if staff_service.is_externally_driven_task(staff.current_task_id):
|
||||
return &"EXTERNAL_TASK"
|
||||
if not _staff_task_reservations_releasable(staff):
|
||||
return &"RESERVATION_RELEASE_FAILED"
|
||||
# Root's temporary boundary R9: an unfinished raid's defender stays until the raid ends.
|
||||
if not _defense_lock_owner(staff.staff_id, &"DEFENDER").is_empty():
|
||||
return &"DEFENSE_LOCKED"
|
||||
# A rescue pair aboard an elevator stays together until a real landing (the
|
||||
# existing rule), so either member is dismissed after that landing.
|
||||
var trip := elevators.get_actor_trip(_staff_path_actor_id(staff.staff_id))
|
||||
if trip.get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED \
|
||||
and not StringName(trip.get(&"companion_id", &"")).is_empty():
|
||||
return &"RESCUE_PAIR_ABOARD"
|
||||
if severance > FinancePolicy.available_funds(economy_service):
|
||||
return &"INSUFFICIENT_FUNDS"
|
||||
return &""
|
||||
|
||||
|
||||
## R3 decided before any change: the removal contract releases every Reservation of the
|
||||
## held Task, and InventoryService.release_reservation fails for a missing or a
|
||||
## consumed Reservation (a released or expired one is a replayed success).
|
||||
func _staff_task_reservations_releasable(staff: StaffRuntime) -> bool:
|
||||
var task := staff_service.get_task(staff.current_task_id)
|
||||
if task == null:
|
||||
return true
|
||||
for reservation_id: StringName in task.reservation_ids:
|
||||
var reservation := inventory_service.get_reservation(reservation_id)
|
||||
if reservation == null or reservation.state == InventoryReservation.State.CONSUMED:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Dismissal of a general employee (FREE_HIRE). All refusals are decided before any
|
||||
## change (SPEC 4.1 c). The existing removal contract, the severance paid from the front
|
||||
## fund with the hidden fund covering the rest (one posting, A14) and the cleanup of
|
||||
## SPEC 6.2 then commit as one operation and are rolled back together on a failure.
|
||||
## change (SPEC 4.1 c). The employee's seat in a boarded rescue pair, the existing
|
||||
## removal contract, the severance paid from the front fund with the hidden fund
|
||||
## covering the rest (one posting, A14) and the cleanup of SPEC 6.2 then commit as one
|
||||
## operation and are rolled back together on a failure.
|
||||
func _execute_dismiss_staff(command: SimulationCommand) -> CommandResult:
|
||||
var raw_staff_id: Variant = command.payload.get(&"staff_id", command.payload.get("staff_id", null))
|
||||
if typeof(raw_staff_id) not in [TYPE_STRING, TYPE_STRING_NAME] or String(raw_staff_id).is_empty():
|
||||
@@ -7001,13 +7037,13 @@ func _execute_dismiss_staff(command: SimulationCommand) -> CommandResult:
|
||||
var severance := FinancePolicy.severance(staff.get_effective_daily_wage())
|
||||
var reason := _staff_dismissal_block_reason(staff, severance)
|
||||
if not reason.is_empty():
|
||||
# R1 and R2 keep the existing removal contract's code and text.
|
||||
# R1, R2 and R3 keep the existing removal contract's code and text.
|
||||
var refusals := {
|
||||
&"UNIQUE": [CommandResult.Code.TRANSACTION_APPLY_FAILED, "Unique Staff cannot be dismissed, sold or permanently removed."],
|
||||
&"EXTERNAL_TASK": [CommandResult.Code.TRANSACTION_APPLY_FAILED, "Finish the employee external operation before removal."],
|
||||
&"RESERVATION_RELEASE_FAILED": [CommandResult.Code.TRANSACTION_APPLY_FAILED, "Staff removal could not release Task Reservations."],
|
||||
&"NOT_GENERAL_EMPLOYEE": [CommandResult.Code.VALIDATION_FAILED, "Only a general employee can be dismissed."],
|
||||
&"DEFENSE_LOCKED": [CommandResult.Code.VALIDATION_FAILED, "Staff is defending an unfinished raid."],
|
||||
&"RESCUE_PAIR_ABOARD": [CommandResult.Code.VALIDATION_FAILED, "A rescue pair aboard an elevator must reach a landing first."],
|
||||
&"INSUFFICIENT_FUNDS": [CommandResult.Code.VALIDATION_FAILED, "Front and hidden funds cannot pay the severance."],
|
||||
}
|
||||
return CommandResult.failed(refusals[reason][0], command.command_id, refusals[reason][1], {
|
||||
@@ -7027,10 +7063,19 @@ func _execute_dismiss_staff(command: SimulationCommand) -> CommandResult:
|
||||
&"id_generator": id_generator.create_snapshot(),
|
||||
}
|
||||
var held_task_id := staff.current_task_id
|
||||
# The existing contract refuses an unreleasable Task Reservation (R3) before it
|
||||
# changes anything; the Inventory release rolls itself back.
|
||||
var reopened: Array = _staff_dismissal_work(staff)[&"reopened"]
|
||||
if not _release_dismissed_rescue_seat(staff_id):
|
||||
_rollback_staff_dismissal(before)
|
||||
return CommandResult.failed(
|
||||
CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id,
|
||||
"Staff dismissal could not release the employee's seat in a boarded rescue pair.",
|
||||
{&"reason": &"RESCUE_SEAT_RELEASE_FAILED", &"staff_id": staff_id, &"error": elevators.last_error}
|
||||
)
|
||||
# The existing contract still refuses an unreleasable Task Reservation (R3, decided
|
||||
# above) as the last guard; nothing it would change survives the rollback.
|
||||
var removal := staff_service.remove_staff(staff_id, &"DISMISSED", true, minute)
|
||||
if not removal.is_success():
|
||||
_rollback_staff_dismissal(before)
|
||||
removal.command_id = command.command_id
|
||||
removal.details[&"reason"] = &"RESERVATION_RELEASE_FAILED"
|
||||
removal.details[&"staff_id"] = staff_id
|
||||
@@ -7070,6 +7115,10 @@ func _execute_dismiss_staff(command: SimulationCommand) -> CommandResult:
|
||||
# settled at the command boundary by the existing rules, not only at the next minute.
|
||||
_sync_facility_staffing(minute)
|
||||
event_service.audit_actor_availability(minute)
|
||||
var reopened_ids: Array[StringName] = []
|
||||
for task: StaffTask in reopened:
|
||||
if not task.is_terminal():
|
||||
reopened_ids.append(task.task_id)
|
||||
return CommandResult.succeeded(command.command_id, {
|
||||
&"staff_id": staff_id,
|
||||
&"severance": severance,
|
||||
@@ -7078,17 +7127,51 @@ func _execute_dismiss_staff(command: SimulationCommand) -> CommandResult:
|
||||
&"unpaid_wages_preserved": int(removal.details.get(&"unpaid_wages_preserved", 0)),
|
||||
&"removed_shift_ids": removal.details.get(&"removed_shift_ids", []),
|
||||
&"removed_override_days": removal.details.get(&"removed_override_days", []),
|
||||
&"reopened_task_ids": [held_task_id] if not held_task_id.is_empty() else [],
|
||||
&"reopened_task_ids": reopened_ids,
|
||||
&"interrupted_rescue_task_ids": interrupted,
|
||||
&"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT),
|
||||
&"back_cash": economy_service.get_balance(FinancePolicy.BACK_CASH),
|
||||
})
|
||||
|
||||
|
||||
## SPEC 6.2.1 and 10: a dismissed member of a rescue pair aboard an elevator leaves the
|
||||
## cabin at the command, as the non-depicted removal; the other member stays aboard, as
|
||||
## the existing rule keeps a live passenger to a real landing. These are the steps of
|
||||
## ElevatorService's own cancellation of a single boarded rider (_cancel_single),
|
||||
## through the public Path and elevator snapshot interfaces: the Path request ends, and
|
||||
## the employee's trip, cabin seat and pairing are removed from the snapshot that
|
||||
## ElevatorService.restore_snapshot then checks against the Path state.
|
||||
func _release_dismissed_rescue_seat(staff_id: StringName) -> bool:
|
||||
var actor_id := _staff_path_actor_id(staff_id)
|
||||
var trip := elevators.get_actor_trip(actor_id)
|
||||
var partner := StringName(trip.get(&"companion_id", &""))
|
||||
if trip.get(&"state", &"") != PathService.ELEVATOR_REQUEST_BOARDED or partner.is_empty():
|
||||
return true
|
||||
if not path_service.complete_elevator_request(StringName(trip[&"connector_id"]), StringName(trip[&"request_id"])):
|
||||
return false
|
||||
var snapshot := elevators.create_snapshot()
|
||||
var trips: Array = []
|
||||
for value: Dictionary in snapshot[&"trips"]:
|
||||
var row := value.duplicate(true)
|
||||
if StringName(row[&"actor_id"]) == actor_id:
|
||||
continue
|
||||
if StringName(row[&"actor_id"]) == partner:
|
||||
row.erase(&"companion_id")
|
||||
trips.append(row)
|
||||
snapshot[&"trips"] = trips
|
||||
for car: Dictionary in snapshot[&"shafts"]:
|
||||
(car[&"boarded"] as Array).erase(actor_id)
|
||||
return elevators.restore_snapshot(snapshot)
|
||||
|
||||
|
||||
func _staff_aboard(staff_id: StringName) -> bool:
|
||||
return elevators.get_actor_trip(_staff_path_actor_id(staff_id)).get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED
|
||||
|
||||
|
||||
## SPEC 6.2.1: no live rescue Task keeps the dismissed employee as its patient or
|
||||
## carrier. The next minute's rule (_advance_staff_rescue) is applied to those Tasks
|
||||
## now: each is interrupted and its elevator requests are cancelled, except that a
|
||||
## carrier already aboard rides on to a real landing. A pickup whose carrier was
|
||||
## now: each is interrupted and its elevator requests are cancelled, except that a live
|
||||
## passenger already aboard rides on to a real landing. A pickup whose carrier was
|
||||
## dismissed was reopened by the removal and stays open for another medic, as that
|
||||
## rule keeps it. Returns null if a Task could not be interrupted.
|
||||
func _interrupt_dismissed_staff_rescue(staff_id: StringName, held_task_id: StringName, minute: int) -> Variant:
|
||||
@@ -7101,13 +7184,12 @@ func _interrupt_dismissed_staff_rescue(staff_id: StringName, held_task_id: Strin
|
||||
continue
|
||||
var patient := staff_service.get_staff(task.target_entity_id)
|
||||
var carrier := staff_service.get_staff(task.assigned_staff_id)
|
||||
var aboard: bool = carrier != null and elevators.get_actor_trip(_staff_path_actor_id(carrier.staff_id)).get(&"state", &"") \
|
||||
== PathService.ELEVATOR_REQUEST_BOARDED
|
||||
if not staff_service.cancel_task(task.task_id, &"RESCUE_INTERRUPTED", minute).is_success():
|
||||
return null
|
||||
interrupted.append(task.task_id)
|
||||
if patient != null: elevators.cancel_actor(_staff_path_actor_id(patient.staff_id))
|
||||
if carrier != null and not aboard: elevators.cancel_actor(_staff_path_actor_id(carrier.staff_id))
|
||||
for member: StaffRuntime in [patient, carrier]:
|
||||
if member != null and not _staff_aboard(member.staff_id):
|
||||
elevators.cancel_actor(_staff_path_actor_id(member.staff_id))
|
||||
return interrupted
|
||||
|
||||
|
||||
@@ -9735,6 +9817,14 @@ func _reserve_staff_task_items(task: StaffTask, absolute_minute: int) -> Diction
|
||||
var before := inventory_service.create_snapshot()
|
||||
var reservation_ids: Array[StringName] = []
|
||||
for item_id: StringName in _sorted_name_keys(task.required_items):
|
||||
# One operation per reservation of the Task: InventoryService never removes a
|
||||
# Reservation, so the count of the Task's earlier ones for the item makes the next
|
||||
# reservation after a release (an interruption, a removal or a dismissal) a new
|
||||
# operation, also within the same minute.
|
||||
var earlier := 0
|
||||
for reservation: InventoryReservation in inventory_service.get_reservations():
|
||||
if reservation.requester_id == task.task_id and reservation.item_id == item_id:
|
||||
earlier += 1
|
||||
var result := inventory_service.reserve_item(
|
||||
inventory_id,
|
||||
item_id,
|
||||
@@ -9743,7 +9833,7 @@ func _reserve_staff_task_items(task: StaffTask, absolute_minute: int) -> Diction
|
||||
&"staff_task",
|
||||
absolute_minute,
|
||||
absolute_minute + 480,
|
||||
StringName("staff.task.reserve.%s.%s" % [task.task_id, item_id])
|
||||
StringName("staff.task.reserve.%s.%s.%d" % [task.task_id, item_id, earlier])
|
||||
)
|
||||
if not bool(result.get(&"ok", false)):
|
||||
inventory_service.restore_snapshot(before)
|
||||
|
||||
Reference in New Issue
Block a user