mirror of
https://github.com/Dadido3/noita-mapcap.git
synced 2024-11-18 17:17:31 +00:00
Add player path tracking and drawing in live mode
This commit is contained in:
parent
0044075cbf
commit
1e5249d436
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -24,6 +24,7 @@
|
|||||||
"mapcap",
|
"mapcap",
|
||||||
"nfnt",
|
"nfnt",
|
||||||
"noita",
|
"noita",
|
||||||
|
"polymorphed",
|
||||||
"prerender",
|
"prerender",
|
||||||
"promptui",
|
"promptui",
|
||||||
"rasterizer",
|
"rasterizer",
|
||||||
|
@ -32,6 +32,7 @@ type imageTile struct {
|
|||||||
pixelErrorSum uint64 // Sum of the difference between the (sub)pixels of all overlapping images. 0 Means that all overlapping images are identical.
|
pixelErrorSum uint64 // Sum of the difference between the (sub)pixels of all overlapping images. 0 Means that all overlapping images are identical.
|
||||||
|
|
||||||
entities []Entity // List of entities that may lie on or near this image tile.
|
entities []Entity // List of entities that may lie on or near this image tile.
|
||||||
|
playerPath *PlayerPath // Contains the player path.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *imageTile) GetImage() (*image.RGBA, error) {
|
func (it *imageTile) GetImage() (*image.RGBA, error) {
|
||||||
@ -101,6 +102,21 @@ func (it *imageTile) GetImage() (*image.RGBA, error) {
|
|||||||
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw player path.
|
||||||
|
if it.playerPath != nil {
|
||||||
|
c := canvas.New(float64(imgRGBA.Rect.Dx()), float64(imgRGBA.Rect.Dy()))
|
||||||
|
ctx := canvas.NewContext(c)
|
||||||
|
ctx.SetCoordSystem(canvas.CartesianIV)
|
||||||
|
ctx.SetCoordRect(canvas.Rect{X: -float64(oldRect.Min.X), Y: -float64(oldRect.Min.Y), W: float64(imgRGBA.Rect.Dx()), H: float64(imgRGBA.Rect.Dy())}, float64(imgRGBA.Rect.Dx()), float64(imgRGBA.Rect.Dy()))
|
||||||
|
|
||||||
|
it.playerPath.Draw(ctx, scaledRect)
|
||||||
|
|
||||||
|
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
||||||
|
r := rasterizer.FromImage(imgRGBA, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
||||||
|
c.Render(r)
|
||||||
|
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
||||||
|
}
|
||||||
|
|
||||||
// Restore the position of the image rectangle.
|
// Restore the position of the image rectangle.
|
||||||
imgRGBA.Rect = scaledRect
|
imgRGBA.Rect = scaledRect
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
var regexFileParse = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
var regexFileParse = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
||||||
|
|
||||||
func loadImages(path string, entities []Entity, scaleDivider int) ([]imageTile, error) {
|
func loadImages(path string, entities []Entity, playerPath *PlayerPath, scaleDivider int) ([]imageTile, error) {
|
||||||
var imageTiles []imageTile
|
var imageTiles []imageTile
|
||||||
|
|
||||||
if scaleDivider < 1 {
|
if scaleDivider < 1 {
|
||||||
@ -60,6 +60,7 @@ func loadImages(path string, entities []Entity, scaleDivider int) ([]imageTile,
|
|||||||
image: image.Rect(x/scaleDivider, y/scaleDivider, (x+width)/scaleDivider, (y+height)/scaleDivider),
|
image: image.Rect(x/scaleDivider, y/scaleDivider, (x+width)/scaleDivider, (y+height)/scaleDivider),
|
||||||
imageMutex: &sync.RWMutex{},
|
imageMutex: &sync.RWMutex{},
|
||||||
entities: entities,
|
entities: entities,
|
||||||
|
playerPath: playerPath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
85
bin/stitch/player-path.go
Normal file
85
bin/stitch/player-path.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2022 David Vogel
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/tdewolff/canvas"
|
||||||
|
)
|
||||||
|
|
||||||
|
var playerPathDisplayStyle = canvas.Style{
|
||||||
|
FillColor: canvas.Transparent,
|
||||||
|
//StrokeColor: color.RGBA{0, 0, 0, 127},
|
||||||
|
StrokeWidth: 3.0,
|
||||||
|
StrokeCapper: canvas.RoundCap,
|
||||||
|
StrokeJoiner: canvas.MiterJoin,
|
||||||
|
DashOffset: 0.0,
|
||||||
|
Dashes: []float64{},
|
||||||
|
FillRule: canvas.NonZero,
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerPathElement struct {
|
||||||
|
From [2]float64 `json:"from"`
|
||||||
|
To [2]float64 `json:"to"`
|
||||||
|
HP float64 `json:"hp"`
|
||||||
|
MaxHP float64 `json:"maxHP"`
|
||||||
|
Polymorphed bool `json:"polymorphed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerPath struct {
|
||||||
|
PathElements []PlayerPathElement
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPlayerPath(path string) (*PlayerPath, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []PlayerPathElement
|
||||||
|
|
||||||
|
jsonDec := json.NewDecoder(file)
|
||||||
|
if err := jsonDec.Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PlayerPath{PathElements: result}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PlayerPath) Draw(c *canvas.Context, imgRect image.Rectangle) {
|
||||||
|
// Set drawing style.
|
||||||
|
c.Style = playerPathDisplayStyle
|
||||||
|
|
||||||
|
for _, pathElement := range p.PathElements {
|
||||||
|
from, to := pathElement.From, pathElement.To
|
||||||
|
|
||||||
|
// Only draw if the path may cross the image rectangle.
|
||||||
|
pathRect := image.Rectangle{image.Point{int(from[0]), int(from[1])}, image.Point{int(to[0]), int(to[1])}}.Canon().Inset(int(-playerPathDisplayStyle.StrokeWidth) - 1)
|
||||||
|
if pathRect.Overlaps(imgRect) {
|
||||||
|
path := &canvas.Path{}
|
||||||
|
path.MoveTo(from[0], from[1])
|
||||||
|
path.LineTo(to[0], to[1])
|
||||||
|
|
||||||
|
if pathElement.Polymorphed {
|
||||||
|
// Set stroke color to typically polymorph color.
|
||||||
|
c.Style.StrokeColor = color.RGBA{127, 50, 83, 127}
|
||||||
|
} else {
|
||||||
|
// Set stroke color depending on HP level.
|
||||||
|
hpFactor := math.Max(math.Min(pathElement.HP/pathElement.MaxHP, 1), 0)
|
||||||
|
hpFactorInv := 1 - hpFactor
|
||||||
|
r, g, b, a := uint8((0*hpFactor+1*hpFactorInv)*127), uint8((1*hpFactor+0*hpFactorInv)*127), uint8(0), uint8(127)
|
||||||
|
c.Style.StrokeColor = color.RGBA{r, g, b, a}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DrawPath(0, 0, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.")
|
var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.")
|
||||||
var flagEntitiesInputPath = flag.String("entities", filepath.Join(".", "..", "..", "output", "entities.json"), "The source path of the entities.json file.")
|
var flagEntitiesInputPath = flag.String("entities", filepath.Join(".", "..", "..", "output", "entities.json"), "The source path of the entities.json file.")
|
||||||
|
var flagPlayerPathInputPath = flag.String("player-path", filepath.Join(".", "..", "..", "output", "player-path.json"), "The source path of the player-path.json file.")
|
||||||
var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image.")
|
var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image.")
|
||||||
var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produce an image with half the side lengths.")
|
var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produce an image with half the side lengths.")
|
||||||
var flagXMin = flag.Int("xmin", 0, "Left bound of the output rectangle. This coordinate is included in the output.")
|
var flagXMin = flag.Int("xmin", 0, "Left bound of the output rectangle. This coordinate is included in the output.")
|
||||||
@ -102,8 +103,32 @@ func main() {
|
|||||||
log.Printf("Got %v entities.", len(entities))
|
log.Printf("Got %v entities.", len(entities))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query the user, if there were no cmd arguments given.
|
||||||
|
if flag.NFlag() == 0 {
|
||||||
|
prompt := promptui.Prompt{
|
||||||
|
Label: "Enter \"player-path.json\" path:",
|
||||||
|
Default: *flagPlayerPathInputPath,
|
||||||
|
AllowEdit: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("Error while getting user input: %v", err)
|
||||||
|
}
|
||||||
|
*flagPlayerPathInputPath = result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load player path if requested.
|
||||||
|
playerPath, err := loadPlayerPath(*flagPlayerPathInputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to load player path: %v", err)
|
||||||
|
}
|
||||||
|
if playerPath != nil && len(playerPath.PathElements) > 0 {
|
||||||
|
log.Printf("Got %v player path entries.", len(playerPath.PathElements))
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("Starting to read tile information at \"%v\"", *flagInputPath)
|
log.Printf("Starting to read tile information at \"%v\"", *flagInputPath)
|
||||||
tiles, err := loadImages(*flagInputPath, entities, *flagScaleDivider)
|
tiles, err := loadImages(*flagInputPath, entities, playerPath, *flagScaleDivider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ local Vec2 = require("noita-api.vec2")
|
|||||||
|
|
||||||
Capture.MapCapturingCtx = Capture.MapCapturingCtx or ProcessRunner.New()
|
Capture.MapCapturingCtx = Capture.MapCapturingCtx or ProcessRunner.New()
|
||||||
Capture.EntityCapturingCtx = Capture.EntityCapturingCtx or ProcessRunner.New()
|
Capture.EntityCapturingCtx = Capture.EntityCapturingCtx or ProcessRunner.New()
|
||||||
|
Capture.PlayerPathCapturingCtx = Capture.PlayerPathCapturingCtx or ProcessRunner.New()
|
||||||
|
|
||||||
---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle.
|
---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle.
|
||||||
---The rectangle is sized and placed in a way that aligns as pixel perfect as possible with the world coordinates.
|
---The rectangle is sized and placed in a way that aligns as pixel perfect as possible with the world coordinates.
|
||||||
@ -158,7 +159,7 @@ function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale)
|
|||||||
---The position in world coordinates.
|
---The position in world coordinates.
|
||||||
---Centered to the grid.
|
---Centered to the grid.
|
||||||
---@type Vec2
|
---@type Vec2
|
||||||
local pos = origin + Vec2(captureGridSize/2, captureGridSize/2)
|
local pos = origin + Vec2(captureGridSize / 2, captureGridSize / 2)
|
||||||
|
|
||||||
---Process main callback.
|
---Process main callback.
|
||||||
---@param ctx ProcessRunnerCtx
|
---@param ctx ProcessRunnerCtx
|
||||||
@ -238,7 +239,7 @@ function Capture:StartCapturingArea(topLeft, bottomRight, captureGridSize, outpu
|
|||||||
---@param ctx ProcessRunnerCtx
|
---@param ctx ProcessRunnerCtx
|
||||||
local function handleDo(ctx)
|
local function handleDo(ctx)
|
||||||
Modification.SetCameraFree(true)
|
Modification.SetCameraFree(true)
|
||||||
ctx.state = {Current = 0, Max = gridSize.x * gridSize.y}
|
ctx.state = { Current = 0, Max = gridSize.x * gridSize.y }
|
||||||
|
|
||||||
while t < tLimit do
|
while t < tLimit do
|
||||||
-- Prematurely stop capturing if that is requested by the context.
|
-- Prematurely stop capturing if that is requested by the context.
|
||||||
@ -251,7 +252,7 @@ function Capture:StartCapturingArea(topLeft, bottomRight, captureGridSize, outpu
|
|||||||
---Position in world coordinates.
|
---Position in world coordinates.
|
||||||
---@type Vec2
|
---@type Vec2
|
||||||
local pos = (hilbertPos + gridTopLeft) * captureGridSize
|
local pos = (hilbertPos + gridTopLeft) * captureGridSize
|
||||||
pos:Add(Vec2(captureGridSize/2, captureGridSize/2)) -- Move to center of grid cell.
|
pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell.
|
||||||
captureScreenshot(pos, true, true, ctx, outputPixelScale)
|
captureScreenshot(pos, true, true, ctx, outputPixelScale)
|
||||||
ctx.state.Current = ctx.state.Current + 1
|
ctx.state.Current = ctx.state.Current + 1
|
||||||
end
|
end
|
||||||
@ -450,7 +451,7 @@ local function createOrOpenEntityCaptureFile()
|
|||||||
local file = io.open("mods/noita-mapcap/output/entities.json", "a")
|
local file = io.open("mods/noita-mapcap/output/entities.json", "a")
|
||||||
if file ~= nil then file:close() end
|
if file ~= nil then file:close() end
|
||||||
|
|
||||||
-- Create or reopen entities CSV file.
|
-- Create or reopen entities JSON file.
|
||||||
file = io.open("mods/noita-mapcap/output/entities.json", "r+b") -- Open for reading (r) and writing (+) in binary mode. r+b will not truncate the file to 0.
|
file = io.open("mods/noita-mapcap/output/entities.json", "r+b") -- Open for reading (r) and writing (+) in binary mode. r+b will not truncate the file to 0.
|
||||||
if file == nil then return nil end
|
if file == nil then return nil end
|
||||||
|
|
||||||
@ -503,6 +504,139 @@ function Capture:StartCapturingEntities(store, modify)
|
|||||||
self.EntityCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
|
self.EntityCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Writes the current player position and other stats onto disk.
|
||||||
|
---@param file file*|nil
|
||||||
|
---@param pos Vec2
|
||||||
|
---@param oldPos Vec2
|
||||||
|
---@param hp number
|
||||||
|
---@param maxHP number
|
||||||
|
---@param polymorphed boolean
|
||||||
|
local function writePlayerPathEntry(file, pos, oldPos, hp, maxHP, polymorphed)
|
||||||
|
if not file then return end
|
||||||
|
|
||||||
|
local struct = {
|
||||||
|
from = oldPos,
|
||||||
|
to = pos,
|
||||||
|
hp = hp,
|
||||||
|
maxHP = maxHP,
|
||||||
|
polymorphed = polymorphed,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Some hacky way to generate valid JSON that doesn't break when the game crashes.
|
||||||
|
-- Well, as long as it does not crash between write and flush.
|
||||||
|
if file:seek("end") == 0 then
|
||||||
|
-- First line.
|
||||||
|
file:write("[\n\t", JSON.Marshal(struct), "\n", "]")
|
||||||
|
else
|
||||||
|
-- Following lines.
|
||||||
|
file:seek("end", -2) -- Seek a few bytes back, so we can overwrite some stuff.
|
||||||
|
file:write(",\n\t", JSON.Marshal(struct), "\n", "]")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure everything is written to disk before noita decides to crash.
|
||||||
|
file:flush()
|
||||||
|
end
|
||||||
|
|
||||||
|
---
|
||||||
|
---@return file*|nil
|
||||||
|
local function createOrOpenPlayerPathCaptureFile()
|
||||||
|
-- Make sure the file exists.
|
||||||
|
local file = io.open("mods/noita-mapcap/output/player-path.json", "a")
|
||||||
|
if file ~= nil then file:close() end
|
||||||
|
|
||||||
|
-- Create or reopen JSON file.
|
||||||
|
file = io.open("mods/noita-mapcap/output/player-path.json", "r+b") -- Open for reading (r) and writing (+) in binary mode. r+b will not truncate the file to 0.
|
||||||
|
if file == nil then return nil end
|
||||||
|
|
||||||
|
return file
|
||||||
|
end
|
||||||
|
|
||||||
|
---Starts capturing the player path.
|
||||||
|
---Use `Capture.PlayerPathCapturingCtx` to stop, control or view the progress.
|
||||||
|
---@param interval integer|nil -- Wait time between captures in frames.
|
||||||
|
function Capture:StartCapturingPlayerPath(interval)
|
||||||
|
interval = interval or 20
|
||||||
|
|
||||||
|
local file
|
||||||
|
local oldPos
|
||||||
|
|
||||||
|
---Process initialization callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleInit(ctx)
|
||||||
|
-- Create output file if requested.
|
||||||
|
file = createOrOpenPlayerPathCaptureFile()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Process main callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleDo(ctx)
|
||||||
|
repeat
|
||||||
|
-- Get player entity, even if it is polymorphed.
|
||||||
|
|
||||||
|
-- For some reason Noita crashes when querying the "is_player" GameStatsComponent value on a freshly polymorphed entity found by its "player_unit" tag.
|
||||||
|
-- It seems that the entity can still be found by the tag, but its components/values can't be accessed anymore.
|
||||||
|
-- Solution: Don't do that.
|
||||||
|
|
||||||
|
---@type NoitaEntity|nil
|
||||||
|
local playerEntity
|
||||||
|
|
||||||
|
-- Try to find the regular player entity.
|
||||||
|
for _, entity in ipairs(EntityAPI.GetWithTag("player_unit")) do
|
||||||
|
playerEntity = entity
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If no player_unit entity was found, check if the player is any of the polymorphed entities.
|
||||||
|
if not playerEntity then
|
||||||
|
for _, entity in ipairs(EntityAPI.GetWithTag("polymorphed")) do
|
||||||
|
local gameStatsComponent = entity:GetFirstComponent("GameStatsComponent")
|
||||||
|
if gameStatsComponent and gameStatsComponent:GetValue("is_player") then
|
||||||
|
playerEntity = entity
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Found some player entity.
|
||||||
|
if playerEntity then
|
||||||
|
-- Get position.
|
||||||
|
local x, y, rotation, scaleX, scaleY = playerEntity:GetTransform()
|
||||||
|
local pos = Vec2(x, y)
|
||||||
|
|
||||||
|
-- Get some other stats from the player.
|
||||||
|
local damageModel = playerEntity:GetFirstComponent("DamageModelComponent")
|
||||||
|
local hp, maxHP
|
||||||
|
if damageModel then
|
||||||
|
hp, maxHP = damageModel:GetValue("hp"), damageModel:GetValue("max_hp")
|
||||||
|
end
|
||||||
|
local polymorphed = playerEntity:HasTag("polymorphed")
|
||||||
|
|
||||||
|
if oldPos then writePlayerPathEntry(file, pos, oldPos, hp, maxHP, polymorphed) end
|
||||||
|
oldPos = pos
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(interval)
|
||||||
|
until ctx:IsStopping()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Process end callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleEnd(ctx)
|
||||||
|
if file then file:close() end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Error handler callback.
|
||||||
|
---@param err string
|
||||||
|
---@param scope "init"|"do"|"end"
|
||||||
|
local function handleErr(err, scope)
|
||||||
|
print(string.format("Failed to capture player path: %s", err))
|
||||||
|
Message:ShowRuntimeError("PlayerPathCaptureError", "Failed to capture player path:", tostring(err))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run process, if there is no other running right now.
|
||||||
|
self.PlayerPathCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
|
||||||
|
end
|
||||||
|
|
||||||
---Starts the capturing process based on user/mod settings.
|
---Starts the capturing process based on user/mod settings.
|
||||||
function Capture:StartCapturing()
|
function Capture:StartCapturing()
|
||||||
Message:CatchException("Capture:StartCapturing", function()
|
Message:CatchException("Capture:StartCapturing", function()
|
||||||
@ -513,6 +647,7 @@ function Capture:StartCapturing()
|
|||||||
|
|
||||||
if mode == "live" then
|
if mode == "live" then
|
||||||
self:StartCapturingLive(outputPixelScale)
|
self:StartCapturingLive(outputPixelScale)
|
||||||
|
self:StartCapturingPlayerPath(5) -- Capture player path with an interval of 5 frames.
|
||||||
elseif mode == "area" then
|
elseif mode == "area" then
|
||||||
local area = ModSettingGet("noita-mapcap.area")
|
local area = ModSettingGet("noita-mapcap.area")
|
||||||
if area == "custom" then
|
if area == "custom" then
|
||||||
@ -558,4 +693,5 @@ end
|
|||||||
function Capture:StopCapturing()
|
function Capture:StopCapturing()
|
||||||
self.EntityCapturingCtx:Stop()
|
self.EntityCapturingCtx:Stop()
|
||||||
self.MapCapturingCtx:Stop()
|
self.MapCapturingCtx:Stop()
|
||||||
|
self.PlayerPathCapturingCtx:Stop()
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user