This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,10 @@
{
"lint": {
"SameLineStatement": "fatal",
"FunctionUnused": "fatal",
//"LocalShadow": "fatal",
//"LocalUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,68 @@
--[[
A helper function to define a Rodux action creator with an associated name.
Normally when creating a Rodux action, you can just create a function:
return function(value)
return {
type = "MyAction",
value = value,
}
end
And then when you check for it in your reducer, you either use a constant,
or type out the string name:
if action.type == "MyAction" then
-- change some state
end
Typos here are a remarkably common bug. We also have the issue that there's
no link between reducers and the actions that they respond to!
`Action` (this helper) provides a utility that makes this a bit cleaner.
Instead, define your Rodux action like this:
return Action("MyAction", function(value)
return {
value = value,
}
end)
We no longer need to add the `type` field manually.
Additionally, the returned action creator now has a 'name' property that can
be checked by your reducer:
local MyAction = require(Reducers.MyAction)
...
if action.type == MyAction.name then
-- change some state!
end
Now we have a clear link between our reducers and the actions they use, and
if we ever typo a name, we'll get a warning in LuaCheck as well as an error
at runtime!
]]
return function(name, fn)
assert(type(name) == "string", "A name must be provided to create an Action")
assert(type(fn) == "function", "A function must be provided to create an Action")
return setmetatable({
name = name,
}, {
__call = function(self, ...)
local result = fn(...)
assert(type(result) == "table", "An action must return a table")
result.type = name
return result
end
})
end
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("ActionBindingsUpdateSearchFilter", function(searchTerm)
return {
searchTerm = searchTerm
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("ChangeDevConsoleSize", function(newSize)
return {
newSize = newSize
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientLogUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientMemoryUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientNetworkUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ClientScriptsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("DataStoresUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerJobsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerLogUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerMemoryUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,9 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerNetworkUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerScriptsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("ServerStatsUpdateSearchFilter", function(searchTerm, filterTypes)
return {
searchTerm = searchTerm,
filterTypes = filterTypes
}
end)
@@ -0,0 +1,8 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetActiveTab", function(tabListIndex, isClientView)
return {
newTabIndex = tabListIndex,
isClientView = isClientView
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetDevConsoleMinimized", function(minimize)
return {
isMinimized = minimize
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetDevConsolePosition", function(pos)
return {
position = pos
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetDevConsoleVisibility", function(visibility)
return {
isVisible = visibility
}
end)
@@ -0,0 +1,9 @@
local Action = require(script.Parent.Parent.Action)
return Action(script.Name, function(waitingForRecording, lastFileOutputLocation)
return {
waitingForRecording = waitingForRecording,
lastFileOutputLocation = lastFileOutputLocation or "",
}
end)
@@ -0,0 +1,9 @@
local Action = require(script.Parent.Parent.Action)
return Action("SetTabList", function(tabList, initIndex, isDeveloperView)
return {
tabList = tabList,
initIndex = initIndex,
isDeveloperView = isDeveloperView,
}
end)
@@ -0,0 +1,7 @@
local Action = require(script.Parent.Parent.Action)
return Action("UpdateAveragePing", function(newAveragePing)
return {
AveragePing = newAveragePing
}
end)
@@ -0,0 +1,151 @@
local CircularBuffer = {}
CircularBuffer.__index = CircularBuffer
function CircularBuffer.new(size)
assert(size, "Cannot initialize CircularBuffer with nil")
assert(size > 0, "Cannot initialize CircularBuffer to size < 1")
local self = {}
setmetatable(self, CircularBuffer)
self._data = {}
self._backIndex = 0
self._maxSize = size
return self
end
function CircularBuffer:reset()
self._data = {}
self._backIndex = 0
end
function CircularBuffer:getSize()
return #self._data
end
function CircularBuffer:getMaxSize()
return self._maxSize
end
function CircularBuffer:setSize(newSize)
assert(newSize, "Cannot set CircularBuffer with nil")
assert(newSize > 0, "Cannot set CircularBuffer to size < 1")
if newSize == self._maxSize then
return
end
local it = self:iterator()
local msg = it:next()
local sorted = {}
local ind = 0
while msg and ind < newSize do
local nextInd = ind + 1
sorted[nextInd] = {
entry = msg
}
if sorted[ind] then
sorted[ind]._next = sorted[nextInd]
end
ind = nextInd
msg = it:next()
end
self._data = sorted
self._backIndex = ind
self._maxSize = newSize
end
function CircularBuffer:getFrontIndex()
local front = self._backIndex + 1
if not self._data[front] then
return 1
end
return front
end
function CircularBuffer:front()
local front = self:getFrontIndex()
if self._data[front] then
return self._data[front].entry
end
return nil
end
function CircularBuffer:back()
if self._data[self._backIndex] then
return self._data[self._backIndex].entry
end
return nil
end
function CircularBuffer:iterator()
local front = self._data[self:getFrontIndex()]
local iterator = {
data = front,
next = function (self)
local retVal = self.data
if retVal then
self.data = self.data._next
end
return retVal and retVal.entry
end
}
return iterator
end
function CircularBuffer:getData()
return self._data
end
function CircularBuffer:at(ind)
assert(ind, "Cannot index CircularBuffer with nil")
local index = self:getFrontIndex()
index = (index + ind - 2) % self._maxSize + 1
if self._data[index] then
return self._data[index].entry
end
return nil
end
function CircularBuffer:reverseAt(ind)
local index = self._backIndex
index = (index - ind) % self._maxSize + 1
if self._data[index] then
return self._data[index].entry
end
return nil
end
-- returns the ejected element if newData overwrites
-- the previous front element
function CircularBuffer:push_back(newData)
local currBackIndex = self._backIndex
local newBackIndex = self._backIndex + 1
if newBackIndex > self._maxSize then
newBackIndex = 1
end
local overwrittenData = self._data[newBackIndex]
self._data[newBackIndex] = {
entry = newData
}
if currBackIndex > 0 then
self._data[currBackIndex]._next = self._data[newBackIndex]
if overwrittenData then
overwrittenData._next = nil
end
end
self._backIndex = newBackIndex
return overwrittenData and overwrittenData.entry
end
return CircularBuffer
@@ -0,0 +1,144 @@
return function()
local CircularBuffer = require(script.Parent.CircularBuffer)
it("should not allow initialize to size 0", function()
expect(function()
local buffer = CircularBuffer.new(0)
end).to.throw()
expect(function()
local buffer = CircularBuffer.new()
end).to.throw()
end)
it("should push_back() past its max size and loop over itself", function()
local buffer = CircularBuffer.new(1)
buffer:push_back("test1")
buffer:push_back("test2")
buffer:push_back("test3")
expect(buffer:getSize()).to.equal(1)
expect(buffer:front()).to.equal("test3")
buffer:setSize(5)
buffer:reset()
expect(buffer:getSize()).to.equal(0)
local expectedData = { 1, 2, 3, 4, 5 }
for _, v in ipairs(expectedData) do
buffer:push_back(v)
end
expect(buffer:front()).to.equal(1)
buffer:push_back(6)
expect(buffer:front()).to.equal(2)
end)
it("should sort and clip data during setSize() where applicable", function()
local buffer = CircularBuffer.new(100)
for i = 1, 100 do
buffer:push_back(i)
end
expect(buffer:getSize()).to.equal(100)
buffer:setSize(1)
expect(buffer:getSize()).to.equal(1)
buffer:setSize(100)
expect(buffer:getSize()).to.equal(1)
end)
it("should maintain the same front() value until it loops over itself", function()
local buffer = CircularBuffer.new(10)
expect(buffer:front()).to.equal(nil)
for i = 1, 10 do
buffer:push_back(i)
expect(buffer:front()).to.equal(1)
end
buffer:push_back(11)
expect(buffer:front()).to.equal(2)
buffer:push_back(12)
expect(buffer:front()).to.equal(3)
end)
it("should always return the last push_back() value when calling back()", function()
local buffer = CircularBuffer.new(10)
expect(buffer:back()).to.equal(nil)
for i = 1, 20 do
buffer:push_back(i)
expect(buffer:back()).to.equal(i)
end
end)
it("should iterate via iterator and terminate with a nil", function()
local buffer = CircularBuffer.new(100)
for i = 1, 100 do
buffer:push_back(i)
end
local it = buffer:iterator()
local val = it:next()
local count = 1
while val do
expect(val).to.equal(count)
val = it:next()
count = count + 1
end
expect(val).to.equal(nil)
end)
it("should maintain expected getData() after push_back()", function()
local buffer = CircularBuffer.new(5)
local expectedData = {1, 2, 3, 4, 5}
for _, v in ipairs(expectedData) do
buffer:push_back(v)
end
local data = buffer:getData()
for i,v in ipairs(expectedData) do
expect(data[i].entry).to.equal(v)
end
buffer:push_back(6)
local newExpectedData = {6, 2, 3, 4, 5}
data = buffer:getData()
for i,v in ipairs(newExpectedData) do
expect(data[i].entry).to.equal(v)
end
end)
it("should loop to access the correct values when using at() and reverseAt", function()
local buffer = CircularBuffer.new(10)
local expectedData = {2, 4, 6, 8, 0, 9, 7, 5, 3, 1}
for _,v in ipairs(expectedData) do
buffer:push_back(v)
end
for i, v in ipairs(expectedData) do
expect(buffer:at(i)).to.equal(v)
end
expect(buffer:at(0)).to.equal(1)
expect(buffer:at(100)).to.equal(1)
expect(buffer:at(-100)).to.equal(1)
expect(buffer:at(0)).to.equal(1)
expect(buffer:at(100)).to.equal(1)
expect(buffer:at(-100)).to.equal(1)
expect(buffer:reverseAt(1)).to.equal(1)
expect(buffer:reverseAt(5)).to.equal(9)
expect(buffer:reverseAt(0)).to.equal(2)
expect(buffer:reverseAt(100)).to.equal(2)
expect(buffer:reverseAt(-100)).to.equal(2)
end)
end
@@ -0,0 +1,350 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local HeaderButton = require(Components.HeaderButton)
local CellLabel = require(Components.CellLabel)
local Constants = require(script.Parent.Parent.Parent.Constants)
local GeneralFormatting = Constants.GeneralFormatting
local LINE_WIDTH = GeneralFormatting.LineWidth
local LINE_COLOR = GeneralFormatting.LineColor
local ActionBindingsFormatting = Constants.ActionBindingsFormatting
local HEADER_NAMES = ActionBindingsFormatting.ChartHeaderNames
local CELL_WIDTHS = ActionBindingsFormatting.ChartCellWidths
local HEADER_HEIGHT = ActionBindingsFormatting.HeaderFrameHeight
local ENTRY_HEIGHT = ActionBindingsFormatting.EntryFrameHeight
local CELL_PADDING = ActionBindingsFormatting.CellPadding
local MIN_FRAME_WIDTH = ActionBindingsFormatting.MinFrameWidth
local IS_CORE_STR = "Core"
local IS_DEVELOPER_STR = "Developer"
local NON_FOUND_ENTRIES_STR = "No ActionBindings Found"
-- create table of offsets and sizes for each cell
local totalCellWidth = 0
for _, cellWidth in ipairs(CELL_WIDTHS) do
totalCellWidth = totalCellWidth + cellWidth
end
local currOffset = -totalCellWidth
local cellOffset = {}
local headerCellSize = {}
local entryCellSize = {}
currOffset = currOffset / 2
table.insert(cellOffset, UDim2.new(0, CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(.5, currOffset - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(.5, currOffset - CELL_PADDING, 0, ENTRY_HEIGHT))
for _, cellWidth in ipairs(CELL_WIDTHS) do
table.insert(cellOffset,UDim2.new(.5, currOffset + CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, ENTRY_HEIGHT))
currOffset = currOffset + cellWidth
end
table.insert(cellOffset,UDim2.new(.5, currOffset + CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(.5, (-totalCellWidth / 2) - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(.5, (-totalCellWidth / 2) - CELL_PADDING, 0, ENTRY_HEIGHT))
local verticalOffsets = {}
for i, offset in ipairs(cellOffset) do
verticalOffsets[i] = UDim2.new(
offset.X.Scale,
offset.X.Offset - CELL_PADDING,
offset.Y.Scale,
offset.Y.Offset)
end
local ActionBindingsChart = Roact.Component:extend("ActionBindingsChart")
local function constructHeader(onSortChanged, width)
local header = {}
for ind, name in ipairs(HEADER_NAMES) do
header[name] = Roact.createElement(HeaderButton, {
text = name,
size = headerCellSize[ind],
pos = cellOffset[ind],
sortfunction = onSortChanged,
})
end
header["upperHorizontalLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
header["lowerHorizontalLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, -LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
for ind = 2, #verticalOffsets do
local key = string.format("VerticalLine_%d",ind)
header[key] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = verticalOffsets[ind],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
return Roact.createElement("Frame", {
Size = UDim2.new(0, width, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
}, header)
end
local function constructEntry(entry, width, layoutOrder)
local name = entry.name
local actionInfo = entry.actionInfo
-- the last element is special cased because the data in the
-- string is passed in as value in the table
-- use tostring to convert the enum into an actual string also because it's used twice
local enumStr = tostring(actionInfo["inputTypes"][1])
local isCoreString = IS_CORE_STR
if actionInfo["isCore"] then
isCoreString = IS_DEVELOPER_STR
end
local row = {}
for i = 2,#verticalOffsets do
local key = string.format("line_%d",i)
row[key] = Roact.createElement("Frame", {
Size = UDim2.new(0,LINE_WIDTH,1,0),
Position = verticalOffsets[i],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
row[name] = Roact.createElement(CellLabel, {
text = enumStr,
size = entryCellSize[1],
pos = cellOffset[1],
})
row.priorityLevel = Roact.createElement(CellLabel, {
text = actionInfo["priorityLevel"],
size = entryCellSize[2],
pos = cellOffset[2],
})
row.isCore = Roact.createElement(CellLabel, {
text = isCoreString,
size = entryCellSize[3],
pos = cellOffset[3],
})
row.actionName = Roact.createElement(CellLabel, {
text = name,
size = entryCellSize[4],
pos = cellOffset[4],
})
row.inputTypes = Roact.createElement(CellLabel, {
text = enumStr,
size = entryCellSize[5],
pos = cellOffset[5],
})
row.lowerHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
return Roact.createElement("Frame", {
Size = UDim2.new(0, width, 0, ENTRY_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
},row)
end
function ActionBindingsChart:init(props)
local initBindings = props.ActionBindingsData:getCurrentData()
self.onSortChanged = function(sortType)
local currSortType = props.ActionBindingsData:getSortType()
if sortType == currSortType then
self:setState({
reverseSort = not self.state.reverseSort
})
else
props.ActionBindingsData:setSortType(sortType)
self:setState({
reverseSort = false,
})
end
end
self.onCanvasPosChanged = function()
local canvasPos = self.scrollingRef.current.CanvasPosition
if self.state.canvasPos ~= canvasPos then
self:setState({
canvasPos = canvasPos,
})
end
end
self.scrollingRef = Roact.createRef()
self.state = {
actionBindingEntries = initBindings,
reverseSort = false,
}
end
function ActionBindingsChart:willUpdate()
if self.canvasPosConnector then
self.canvasPosConnector:Disconnect()
end
end
function ActionBindingsChart:didUpdate()
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
local absSize = self.scrollingRef.current.AbsoluteSize
local currAbsSize = self.state.absScrollSize
if absSize.X ~= currAbsSize.X or
absSize.Y ~= currAbsSize.Y then
self:setState({
absScrollSize = absSize,
})
end
end
end
function ActionBindingsChart:didMount()
self.bindingsUpdated = self.props.ActionBindingsData:Signal():Connect(function(bindingsData)
self:setState({
actionBindingEntries = bindingsData
})
end)
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = self.scrollingRef.current.CanvasPosition,
})
end
end
function ActionBindingsChart:willUnmount()
self.bindingsUpdated:Disconnect()
self.bindingsUpdated = nil
self.canvasPosConnector:Disconnect()
self.canvasPosConnector = nil
end
function ActionBindingsChart:render()
local entries = {}
local searchTerm = self.props.searchTerm
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local entryList = self.state.actionBindingEntries
local reverseSort = self.state.reverseSort
local canvasPos = self.state.canvasPos
local absScrollSize = self.state.absScrollSize
local frameWidth = absScrollSize and math.max(absScrollSize.X, MIN_FRAME_WIDTH) or MIN_FRAME_WIDTH
entries["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
local totalEntries = #entryList
local canvasHeight = 0
if absScrollSize and canvasPos then
local paddingHeight = -1
local usedFrameSpace = 0
local count = 0
for ind, entry in ipairs(entryList) do
local foundTerm = false
if searchTerm then
local enumStr = tostring(entry.actionInfo["inputTypes"][1])
foundTerm = string.find(enumStr:lower(), searchTerm:lower()) ~= nil
foundTerm = foundTerm or string.find(entry.name:lower(), searchTerm:lower()) ~= nil
end
if not searchTerm or foundTerm then
if canvasHeight + ENTRY_HEIGHT >= canvasPos.Y then
if usedFrameSpace < absScrollSize.Y then
local entryLayoutOrder = reverseSort and (totalEntries - ind) or ind
entries[ind] = constructEntry(entry, frameWidth, entryLayoutOrder + 1)
end
if paddingHeight < 0 then
paddingHeight = canvasHeight
else
usedFrameSpace = usedFrameSpace + ENTRY_HEIGHT
end
count = count + 1
end
canvasHeight = canvasHeight + ENTRY_HEIGHT
end
end
if count == 0 then
entries["NoneFound"] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Text = NON_FOUND_ENTRIES_STR,
TextColor3 = LINE_COLOR,
BackgroundTransparency = 1,
LayoutOrder = 1,
})
else
entries["WindowingPadding"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, paddingHeight),
BackgroundTransparency = 1,
LayoutOrder = 1,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
ClipsDescendants = true,
LayoutOrder = layoutOrder,
}, {
Header = constructHeader(self.onSortChanged, frameWidth),
MainChart = Roact.createElement("ScrollingFrame", {
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
Size = UDim2.new(1, 0, 1, - HEADER_HEIGHT),
CanvasSize = UDim2.new(0, frameWidth, 0, canvasHeight),
ScrollBarThickness = 6,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
[Roact.Ref] = self.scrollingRef
}, entries),
})
end
return DataConsumer(ActionBindingsChart, "ActionBindingsData")
@@ -0,0 +1,31 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local ActionBindingsChart = require(script.Parent.ActionBindingsChart)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
ActionBindingsChart = Roact.createElement(ActionBindingsChart)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,158 @@
local ContextActionService = game:GetService("ContextActionService")
local Signal = require(script.Parent.Parent.Parent.Signal)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.ActionBindingsFormatting.ChartHeaderNames
local SORT_COMPARATOR = {
[HEADER_NAMES[1]] = function(a,b) -- "Name"
return a.counter < b.counter
end,
[HEADER_NAMES[2]] = function(a,b) -- "Priority"
if a.actionInfo.priorityLevel == b.actionInfo.priorityLevel then
return a.counter < b.counter
end
return a.actionInfo.priorityLevel < b.actionInfo.priorityLevel
end,
[HEADER_NAMES[3]] = function(a,b) -- "Security"
if a.actionInfo.isCore == b.actionInfo.isCore then
return a.counter < b.counter
else
return a.actionInfo.isCore
end
end,
[HEADER_NAMES[4]] = function(a,b) -- "Action Name"
return a.name:lower() < b.name:lower()
end,
[HEADER_NAMES[5]] = function(a,b) -- "Input Types"
return tostring(a.actionInfo.inputTypes[1]) < tostring(b.actionInfo.inputTypes[1])
end,
}
local ActionBindingsData = {}
ActionBindingsData.__index = ActionBindingsData
function ActionBindingsData.new()
local self = {}
setmetatable(self, ActionBindingsData)
self._bindingsUpdated = Signal.new()
self._bindingsData = {}
self._bindingCounter = 0
self._sortedBindingData = {}
self._sortType = HEADER_NAMES[1] -- Name
self._isRunning = false
return self
end
function ActionBindingsData:setSortType(sortType)
if SORT_COMPARATOR[sortType] then
self._sortType = sortType
table.sort(self._sortedBindingData, SORT_COMPARATOR[self._sortType])
self._bindingsUpdated:Fire(self._sortedBindingData)
else
error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2)
end
end
function ActionBindingsData:getSortType()
return self._sortType
end
function ActionBindingsData:Signal()
return self._bindingsUpdated
end
function ActionBindingsData:getCurrentData()
return self._sortedBindingData
end
-- this funciton will require some extra work to handle the
-- case a entry insert occurs during the end of the list
function ActionBindingsData:updateBindingDataEntry(name, info)
if info == nil then
--remove element and clean up sorted
self._bindingsData[name] = nil
for i, v in pairs(self._sortedBindingData) do
if v.name == name then
table.remove(self._sortedBindingData, i)
return
end
end
elseif not self._bindingsData[name] then
self._bindingCounter = self._bindingCounter + 1
self._bindingsData[name] = info
local newEntry = {
name = name,
actionInfo = self._bindingsData[name],
counter = self._bindingCounter,
}
table.insert(self._sortedBindingData, newEntry)
else
self._bindingsData[name] = info
end
end
function ActionBindingsData:isRunning()
return self._isRunning
end
function ActionBindingsData:start()
local boundActions = ContextActionService:GetAllBoundActionInfo()
for actionName, actionInfo in pairs(boundActions) do
actionInfo.isCore = false
self:updateBindingDataEntry(actionName, actionInfo)
end
local boundCoreActions = ContextActionService:GetAllBoundCoreActionInfo()
for actionName, actionInfo in pairs(boundCoreActions) do
actionInfo.isCore = true
self:updateBindingDataEntry(actionName, actionInfo)
end
if not self._actionChangedConnection then
self._actionChangedConnection = ContextActionService.BoundActionChanged:connect(
function(actionName, changeName, changeTable)
self:updateBindingDataEntry(actionName, nil)
self:updateBindingDataEntry(changeName, changeTable)
self._bindingsUpdated:Fire(self._sortedBindingData)
end)
end
if not self._actionAddedConnection then
self._actionAddedConnection = ContextActionService.BoundActionAdded:connect(
function(actionName, createTouchButton, actionInfo, isCore)
actionInfo.isCore = isCore
self:updateBindingDataEntry(actionName, actionInfo)
self._bindingsUpdated:Fire(self._sortedBindingData)
end)
end
if not self._actionRemovedConnection then
self._actionRemovedConnection = ContextActionService.BoundActionRemoved:connect(
function(actionName, actionInfo, isCore)
self:updateBindingDataEntry(actionName, nil)
self._bindingsUpdated:Fire(self._sortedBindingData)
end)
end
self._isRunning = true
end
function ActionBindingsData:stop()
if self.actionChangedConnector then
self.actionChangedConnector:Disconnect()
self.actionChangedConnector = nil
end
if self.actionAddedConnector then
self.actionAddedConnector:Disconnect()
self.actionAddedConnector = nil
end
if self.actionRemovedConnector then
self.actionRemovedConnector:Disconnect()
self.actionRemovedConnector = nil
end
self._isRunning = false
end
return ActionBindingsData
@@ -0,0 +1,105 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ActionBindingsChart = require(Components.ActionBindings.ActionBindingsChart)
local UtilAndTab = require(Components.UtilAndTab)
local Actions = script.Parent.Parent.Parent.Actions
local ActionBindingsUpdateSearchFilter = require(Actions.ActionBindingsUpdateSearchFilter)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MainViewActionBindings = Roact.Component:extend("MainViewActionBindings")
function MainViewActionBindings:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onSearchTermChanged = function(newSearchTerm)
self.props.dispatchActionBindingsUpdateSearchFilter(newSearchTerm, {})
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0
}
end
function MainViewActionBindings:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewActionBindings:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewActionBindings:render()
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local searchTerm = self.props.bindingsSearchTerm
local utilTabHeight = self.state.utilTabHeight
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, PADDING),
SortOrder = Enum.SortOrder.LayoutOrder,
}),
UtilAndTab = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onSearchTermChanged = self.onSearchTermChanged,
}),
ActionBindings = utilTabHeight > 0 and Roact.createElement(ActionBindingsChart, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
}),
})
end
local function mapStateToProps(state, props)
return {
bindingsSearchTerm = state.ActionBindingsData.bindingsSearchTerm,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchActionBindingsUpdateSearchFilter = function(searchTerm, filters)
dispatch(ActionBindingsUpdateSearchFilter(searchTerm, filters))
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewActionBindings)
@@ -0,0 +1,37 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewActionBindings = require(script.Parent.MainViewActionBindings)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
ActionBindingsData = {
bindingsSearchTerm = ""
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewActionBindings = Roact.createElement(MainViewActionBindings, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,56 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Immutable = require(script.Parent.Parent.Immutable)
local Constants = require(script.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local ARROW_WIDTH = Constants.GeneralFormatting.ArrowWidth
local CLOSE_ARROW = Constants.Image.RightArrow
local OPEN_ARROW = Constants.Image.DownArrow
local BannerButton = Roact.Component:extend("BannerButton")
function BannerButton:render()
local children = self.props[Roact.Children] or {}
local size = self.props.size
local pos = self.props.pos
local isExpanded = self.props.isExpanded
local layoutOrder = self.props.layoutOrder
local onButtonPress = self.props.onButtonPress
local bannerElements = {
BannerButtonArrow = onButtonPress and Roact.createElement("ImageLabel", {
Image = isExpanded and OPEN_ARROW or CLOSE_ARROW,
BackgroundTransparency = 1,
Size = UDim2.new(0, ARROW_WIDTH, 0, ARROW_WIDTH),
Position = UDim2.new(0, 0, .5, -ARROW_WIDTH / 2),
}),
HorizontalLineTop = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
HorizontalLineBottom = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, -LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}
return Roact.createElement("ImageButton", {
Size = size,
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = onButtonPress,
}, Immutable.JoinDictionaries(bannerElements, children))
end
return BannerButton
@@ -0,0 +1,32 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local TEXT_SIZE = Constants.DefaultFontSize.MainWindow
local TEXT_COLOR = Constants.Color.Text
local MAIN_FONT = Constants.Font.MainWindow
local MAIN_FONT_BOLD = Constants.Font.MainWindowBold
local function CellLabel(props)
local text = props.text
local size = props.size
local pos = props.pos
local bold = props.bold
local layoutOrder = props.layoutOrder
return Roact.createElement("TextLabel", {
Text = text,
TextSize = TEXT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
Font = bold and MAIN_FONT_BOLD or MAIN_FONT,
Size = size,
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
end
return CellLabel
@@ -0,0 +1,70 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local PADDING = Constants.UtilityBarFormatting.CheckBoxInnerPadding
local CheckBox = Roact.Component:extend("CheckBox")
function CheckBox:render()
local checkBoxHeight = self.props.checkBoxHeight
local frameHeight = self.props.frameHeight
local layoutOrder = self.props.layoutOrder
local name = self.props.name
local font = self.props.font
local fontSize = self.props.fontSize
local isSelected = self.props.isSelected
local selectedColor = self.props.selectedColor
local unselectedColor = self.props.unselectedColor
local onCheckBoxClicked = self.props.onCheckBoxClicked
-- this can be replaced with default values once that releases
local image = ""
local borderSize = 1
local backgroundColor = unselectedColor
if isSelected then
image = Constants.Image.Check
borderSize = 0
backgroundColor = selectedColor
end
local textVector = TextService:GetTextSize(name, fontSize, font, Vector2.new(0, frameHeight))
local textWidth = textVector.X
return Roact.createElement("ImageButton", {
Size = UDim2.new(0, checkBoxHeight + textWidth + (PADDING * 2), 0, frameHeight),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = function(rbx)
onCheckBoxClicked(name, not isSelected)
end,
}, {
Icon = Roact.createElement("ImageLabel", {
Image = image,
Size = UDim2.new(0, checkBoxHeight, 0, checkBoxHeight),
Position = UDim2.new(0, 0, .5, -checkBoxHeight / 2),
BackgroundColor3 = backgroundColor,
BackgroundTransparency = 0,
BorderColor3 = Constants.Color.Text,
BorderSizePixel = borderSize,
}),
Text = Roact.createElement("TextLabel", {
Text = name,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
Font = font,
TextSize = fontSize,
Size = UDim2.new(1, -frameHeight, 1, 0),
Position = UDim2.new(0, checkBoxHeight + PADDING, 0, 0),
BackgroundTransparency = 1,
})
})
end
return CheckBox
@@ -0,0 +1,18 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local CheckBox = require(script.Parent.CheckBox)
it("should create and destroy without errors", function()
local element = Roact.createElement(CheckBox, {
name = "",
fontSize = 0,
font = 0,
frameHeight = 0,
checkBoxHeight = 0,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,161 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Components
local CheckBox = require(Components.CheckBox)
local CheckBoxDropDown = require(Components.CheckBoxDropDown)
local Constants = require(script.Parent.Parent.Constants)
local CHECK_BOX_HEIGHT = Constants.UtilityBarFormatting.CheckBoxHeight
local CHECK_BOX_PADDING = Constants.UtilityBarFormatting.CheckBoxInnerPadding * 2
local FILTER_ICON_UNFILLED = Constants.Image.FilterUnfilled
local FILTER_ICON_FILLED = Constants.Image.FilterFilled
local DROP_DOWN_Y_ADJUST = 3
local CheckBoxContainer = Roact.PureComponent:extend("CheckBoxContainer")
function CheckBoxContainer:init()
self.onCheckBoxClicked = function(field, newState)
local onCheckBoxChanged = self.props.onCheckBoxChanged
onCheckBoxChanged(field, newState)
end
-- this is part of the dropdown logic
self.onCheckBoxExpanded = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
expanded = true,
})
end
end
-- this is part of the dropdown logic
self.onCloseCheckBox = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
expanded = false
})
end
end
if not self.props.orderedCheckBoxState then
warn("CheckBoxContainer must be passed a list of Box Names or else it only creates an empty frame")
end
local textWidths = {}
local totalLength = 0
local count = 0
for ind, box in ipairs(self.props.orderedCheckBoxState) do
local textVector = TextService:GetTextSize(
box.name,
Constants.DefaultFontSize.UtilBar,
Constants.Font.UtilBar,
Vector2.new(0, 0)
)
textWidths[ind] = textVector.X
totalLength = totalLength + textVector.X + CHECK_BOX_HEIGHT + CHECK_BOX_PADDING
count = count + 1
end
self.ref = Roact.createRef()
self.state = {
expanded = false,
textWidths = textWidths,
numCheckBoxes = count,
minFullLength = totalLength,
}
end
function CheckBoxContainer:render()
local elements = {}
local frameWidth = self.props.frameWidth
local frameHeight = self.props.frameHeight
local pos = self.props.pos
local layoutOrder = self.props.layoutOrder
local orderedCheckBoxState = self.props.orderedCheckBoxState
local minFullLength = self.state.minFullLength
local expanded = self.state.expanded
local numCheckBoxes = self.state.numCheckBoxes
local anySelected = false
for layoutOrder, box in ipairs(orderedCheckBoxState) do
elements[box.name] = Roact.createElement(CheckBox, {
name = box.name,
font = Constants.Font.UtilBar,
fontSize = Constants.DefaultFontSize.UtilBar,
checkBoxHeight = CHECK_BOX_HEIGHT,
frameHeight = frameHeight,
layoutOrder = layoutOrder,
isSelected = box.state,
selectedColor = Constants.Color.SelectedBlue,
unselectedColor = Constants.Color.UnselectedGray,
onCheckBoxClicked = self.onCheckBoxClicked,
})
anySelected = anySelected or box.state
end
if frameWidth < minFullLength then
elements["CheckBoxLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
})
local showDropDown = self.ref.current and expanded
local dropDownPos
if showDropDown then
local absPos = self.ref.current.AbsolutePosition
-- adding slight y offset to nudge dropdown enough to see button border
dropDownPos = UDim2.new(0, absPos.X, 0, absPos.Y + frameHeight + DROP_DOWN_Y_ADJUST)
end
return Roact.createElement("ImageButton", {
Size = UDim2.new(0, frameHeight, 0, frameHeight),
LayoutOrder = layoutOrder,
Image = showDropDown and FILTER_ICON_FILLED or FILTER_ICON_UNFILLED,
BackgroundTransparency = 1,
BorderColor3 = Constants.Color.Text,
[Roact.Event.InputEnded] = self.onCheckBoxExpanded,
[Roact.Ref] = self.ref,
}, {
DropDown = showDropDown and Roact.createElement(CheckBoxDropDown, {
absolutePosition = dropDownPos,
frameWidth = frameWidth,
elementHeight = frameHeight,
numElements = numCheckBoxes,
onCloseCheckBox = self.onCloseCheckBox
}, elements)
})
else
elements["CheckBoxLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Horizontal,
})
return Roact.createElement("Frame", {
Size = UDim2.new(0,frameWidth,0,frameHeight),
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, elements)
end
end
return CheckBoxContainer
@@ -0,0 +1,15 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local CheckBoxContainer = require(script.Parent.CheckBoxContainer)
it("should create and destroy without errors", function()
local element = Roact.createElement(CheckBoxContainer, {
orderedCheckBoxState = {},
frameWidth = 0,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,51 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local INNER_FRAME_PADDING = 12
local CheckBoxDropDown = Roact.Component:extend("CheckBoxDropDown")
function CheckBoxDropDown:render()
local children = self.props[Roact.Children] or {}
local absolutePosition = self.props.absolutePosition
local frameWidth = self.props.frameWidth
local elementHeight = self.props.elementHeight
local numElements = self.props.numElements
local dropdownTargetGui = self.props.dropdownTargetGui
local onCloseCheckBox = self.props.onCloseCheckBox
local frameHeight = elementHeight * numElements
local outerFrameSize = UDim2.new( 0, frameWidth, 0, (2 * INNER_FRAME_PADDING) + frameHeight)
return Roact.createElement(Roact.Portal, {
-- render the portal into the same ScreenGui as the DevConsole
target = dropdownTargetGui ~= nil and dropdownTargetGui or game:GetService("CoreGui").DevConsoleMaster,
}, {
InputCatcher = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
[Roact.Event.InputEnded] = onCloseCheckBox,
}, {
OuterFrame = Roact.createElement("ImageButton", {
Size = outerFrameSize,
AutoButtonColor = false,
Position = absolutePosition,
BackgroundColor3 = Constants.Color.TextBoxGray,
BackgroundTransparency = 0,
}, {
innerFrame = Roact.createElement("Frame", {
Position = UDim2.new(0, INNER_FRAME_PADDING, 0 , INNER_FRAME_PADDING),
Size = UDim2.new(0,frameWidth, 0, frameHeight),
BackgroundTransparency = 1,
}, children)
})
}),
})
end
return CheckBoxDropDown
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local CheckBoxDropDown = require(script.Parent.CheckBoxDropDown)
it("should create and destroy without errors", function()
local element = Roact.createElement(CheckBoxDropDown, {
elementHeight = 0,
numElements = 0,
dropdownTargetGui = Instance.new("ScreenGui"),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,101 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local FRAME_HEIGHT = Constants.UtilityBarFormatting.FrameHeight
local SMALL_FRAME_HEIGHT = Constants.UtilityBarFormatting.SmallFrameHeight
local CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerButtonWidth
local SMALL_CS_BUTTON_WIDTH = Constants.UtilityBarFormatting.ClientServerDropDownWidth
local FONT = Constants.Font.UtilBar
local FONT_SIZE = Constants.DefaultFontSize.UtilBar
local FONT_COLOR = Constants.Color.Text
local FullScreenDropDownButton = require(script.Parent.FullScreenDropDownButton)
local DropDown = require(script.Parent.DropDown)
local BUTTON_SIZE = UDim2.new(0, CS_BUTTON_WIDTH, 0, FRAME_HEIGHT)
local CLIENT_SERVER_NAMES = {"Client", "Server"}
local ClientServerButton = Roact.Component:extend("ClientServerButton")
function ClientServerButton:init()
self.dropDownCallback = function(index)
if index == 1 then
self.props.onClientButton()
elseif index == 2 then
self.props.onServerButton()
end
end
end
function ClientServerButton:render()
local formFactor = self.props.formFactor
local useDropDown = self.props.useDropDown
local isClientView = self.props.isClientView
local layoutOrder = self.props.layoutOrder
local onServerButton = self.props.onServerButton
local onClientButton = self.props.onClientButton
local serverButtonColor = Constants.Color.SelectedBlue
local clientButtonColor = Constants.Color.UnselectedGray
if isClientView then
clientButtonColor = Constants.Color.SelectedBlue
serverButtonColor = Constants.Color.UnselectedGray
end
if formFactor == Constants.FormFactor.Small then
return Roact.createElement(FullScreenDropDownButton, {
buttonSize = UDim2.new(0, SMALL_CS_BUTTON_WIDTH, 0, SMALL_FRAME_HEIGHT),
dropDownList = CLIENT_SERVER_NAMES,
selectedIndex = isClientView and 1 or 2,
onSelection = self.dropDownCallback,
layoutOrder = layoutOrder,
})
elseif useDropDown then
return Roact.createElement(DropDown, {
buttonSize = UDim2.new(0, SMALL_CS_BUTTON_WIDTH, 0, SMALL_FRAME_HEIGHT),
dropDownList = CLIENT_SERVER_NAMES,
selectedIndex = isClientView and 1 or 2,
onSelection = self.dropDownCallback,
layoutOrder = layoutOrder,
})
else
return Roact.createElement("Frame", {
Size = UDim2.new(0, 2 * CS_BUTTON_WIDTH, 0, FRAME_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
ClientButton = Roact.createElement('TextButton', {
Text = CLIENT_SERVER_NAMES[1],
TextSize = FONT_SIZE,
TextColor3 = FONT_COLOR,
Font = FONT,
Size = BUTTON_SIZE,
AutoButtonColor = false,
BackgroundColor3 = clientButtonColor,
BackgroundTransparency = 0,
LayoutOrder = 1,
[Roact.Event.Activated] = onClientButton,
}),
ServerButton = Roact.createElement('TextButton', {
Text = CLIENT_SERVER_NAMES[2],
TextSize = FONT_SIZE,
TextColor3 = FONT_COLOR,
Font = FONT,
Size = BUTTON_SIZE,
AutoButtonColor = false,
Position = UDim2.new(0, CS_BUTTON_WIDTH, 0, 0),
BackgroundColor3 = serverButtonColor,
BackgroundTransparency = 0,
LayoutOrder = 2,
[Roact.Event.Activated] = onServerButton,
})
})
end
end
return ClientServerButton
@@ -0,0 +1,13 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local ClientServerButton = require(script.Parent.ClientServerButton)
it("should create and destroy without errors", function()
local element = Roact.createElement(ClientServerButton
)
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,41 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Immutable = require(script.Parent.Parent.Immutable)
return function(component, ...)
if not component then
error("Expected component to be passed to connection, got nil.")
end
local targetList = {}
for i = 1, select("#", ...) do
targetList[i] = select(i, ...)
end
local name = string.format("Consumer(%s)_DependsOn_%s",tostring(component), targetList[1] )
local DataConsumer = Roact.Component:extend(name)
function DataConsumer:init()
local contextTable = {}
for _,dataName in pairs(targetList) do
local contextualData = self._context.DevConsoleData[dataName]
if not contextualData then
local errorStr = string.format("%s %s",tostring(dataName),
"could not be found. Make sure DataProvider is above this consumer"
)
error(errorStr)
return
end
contextTable[dataName] = contextualData
end
self.state = contextTable
end
function DataConsumer:render()
local props = Immutable.JoinDictionaries(self.props, self.state)
return Roact.createElement(component, props)
end
return DataConsumer
end
@@ -0,0 +1,71 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Components
local LogData = require(Components.Log.LogData)
local ClientMemoryData = require(Components.Memory.ClientMemoryData)
local ServerMemoryData = require(Components.Memory.ServerMemoryData)
local NetworkData = require(Components.Network.NetworkData)
local ServerScriptsData = require(Components.Scripts.ServerScriptsData)
local DataStoresData = require(Components.DataStores.DataStoresData)
local ServerStatsData = require(Components.ServerStats.ServerStatsData)
local ActionBindingsData = require(Components.ActionBindings.ActionBindingsData)
local ServerJobsData = require(Components.ServerJobs.ServerJobsData)
local FFlagAdminServerLogs = settings():GetFFlag("AdminServerLogs")
local DataProvider = Roact.Component:extend("DataProvider")
function DataProvider:init()
self._context.DevConsoleData = {
ClientLogData = LogData.new(true),
ServerLogData = LogData.new(false),
ClientMemoryData = ClientMemoryData.new(),
ServerMemoryData = ServerMemoryData.new(),
ClientNetworkData = NetworkData.new(true),
ServerNetworkData = NetworkData.new(false),
ServerScriptsData = ServerScriptsData.new(),
DataStoresData = DataStoresData.new(),
ServerStatsData = ServerStatsData.new(),
ActionBindingsData = ActionBindingsData.new(),
ServerJobsData = ServerJobsData.new(),
}
end
function DataProvider:didMount()
if self.props.isDeveloperView and not FFlagAdminServerLogs then
for _, dataProvider in pairs(self._context.DevConsoleData) do
dataProvider:start()
end
else
self._context.DevConsoleData.ClientLogData:start()
self._context.DevConsoleData.ClientMemoryData:start()
end
end
function DataProvider:willUpdate(nextProps, nextState)
if FFlagAdminServerLogs then
if nextProps.isDeveloperView and
not self.props.isDeveloperView then
for _, dataProvider in pairs(self._context.DevConsoleData) do
if not dataProvider:isRunning() then
dataProvider:start()
end
end
end
end
end
function DataProvider:render()
return Roact.oneChild(self.props[Roact.Children])
end
local function mapStateToProps(state, props)
return {
isDeveloperView = state.MainView.isDeveloperView,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, nil)(DataProvider)
@@ -0,0 +1,239 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local CellLabel = require(Components.CellLabel)
local BannerButton = require(Components.BannerButton)
local LineGraph = require(Components.LineGraph)
local Constants = require(script.Parent.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local HEADER_NAMES = Constants.DataStoresFormatting.ChartHeaderNames
local VALUE_CELL_WIDTH = Constants.DataStoresFormatting.ValueCellWidth
local CELL_PADDING = Constants.DataStoresFormatting.CellPadding
local ARROW_PADDING = Constants.DataStoresFormatting.ExpandArrowPadding
local HEADER_HEIGHT = Constants.DataStoresFormatting.HeaderFrameHeight
local ENTRY_HEIGHT = Constants.DataStoresFormatting.EntryFrameHeight
local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight
local NO_DATA_MSG = "Initialize DataStoresService to view DataStore Budget."
local NO_RESULT_SEARCH_STR = Constants.GeneralFormatting.NoResultSearchStr
local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp)
local DataStoresChart = Roact.Component:extend("DataStoresChart")
local function getX(entry)
return entry.time
end
local function getY(entry)
return entry.value
end
local function stringFormatY(value)
return math.ceil(value)
end
function DataStoresChart:init(props)
local currStoresData, currStoresDataCount = props.DataStoresData:getCurrentData()
self.getOnButtonPress = function (name)
return function(rbx, input)
self:setState({
expandedEntry = self.state.expandedEntry ~= name and name
})
end
end
self.state = {
dataStoresData = currStoresData,
dataStoresDataCount = currStoresDataCount,
expandedEntry = nil
}
end
function DataStoresChart:didMount()
self.statsConnector = self.props.DataStoresData:Signal():Connect(function(data, count)
self:setState({
dataStoresData = data,
dataStoresDataCount = count,
})
end)
end
function DataStoresChart:willUnmount()
self.statsConnector:Disconnect()
self.statsConnector = nil
end
function DataStoresChart:render()
local elements = {}
local searchTerm = self.props.searchTerm
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local expandedEntry = self.state.expandedEntry
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
local componentHeight = HEADER_HEIGHT
local datastoreBudget = self.state.dataStoresData
local totalCount = 0
local currLayoutOrder = 1
if datastoreBudget then
for name, data in pairs(datastoreBudget) do
totalCount = totalCount + 1
if not searchTerm or string.find(name:lower(), searchTerm:lower()) ~= nil then
currLayoutOrder = currLayoutOrder + 1
local showGraph = expandedEntry == name
local frameHeight = showGraph and ENTRY_HEIGHT + GRAPH_HEIGHT or ENTRY_HEIGHT
elements[name] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, frameHeight),
BackgroundTransparency = 1,
LayoutOrder = currLayoutOrder,
}, {
DataButton = Roact.createElement(BannerButton, {
size = UDim2.new(1, 0, 0, ENTRY_HEIGHT),
pos = UDim2.new(),
isExpanded = showGraph,
onButtonPress = self.getOnButtonPress(name),
}, {
[name] = Roact.createElement(CellLabel, {
text = name,
size = UDim2.new(1 - VALUE_CELL_WIDTH, -CELL_PADDING - ARROW_PADDING, 1, 0),
pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0),
}),
Data = Roact.createElement(CellLabel, {
text = data.dataSet:back().value,
size = UDim2.new(VALUE_CELL_WIDTH , -CELL_PADDING, 1, 0),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, CELL_PADDING, 0, 0),
}),
VerticalLine = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 0, ENTRY_HEIGHT),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
lowerHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
Graph = showGraph and Roact.createElement(LineGraph, {
pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT),
size = UDim2.new(1, 0, 1, -ENTRY_HEIGHT),
graphData = data.dataSet,
maxY = data.max,
minY = data.min,
getX = getX,
getY = getY,
axisLabelX = "Timestamp",
axisLabelY = name,
stringFormatX = convertTimeStamp,
stringFormatY = stringFormatY,
}),
})
componentHeight = componentHeight + ENTRY_HEIGHT
end
end
end
if currLayoutOrder == 1 then
if totalCount == 0 then
return Roact.createElement("TextLabel", {
Size = size,
Text = NO_DATA_MSG,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
else
local noResultSearchStr = string.format(NO_RESULT_SEARCH_STR, searchTerm)
elements["emptyResult"] = Roact.createElement("TextLabel", {
Size = size,
Text = noResultSearchStr,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
Header = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = 1,
}, {
[HEADER_NAMES[1]] = Roact.createElement(CellLabel, {
text = HEADER_NAMES[1],
size = UDim2.new(1 - VALUE_CELL_WIDTH, -CELL_PADDING - ARROW_PADDING, 1, 0),
pos = UDim2.new(0, CELL_PADDING + ARROW_PADDING, 0, 0),
}),
[HEADER_NAMES[2]] = Roact.createElement(CellLabel, {
text = HEADER_NAMES[2],
size = UDim2.new(VALUE_CELL_WIDTH , -CELL_PADDING, 1, 0),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, CELL_PADDING, 0, 0),
}),
upperHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
vertical = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
lowerHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
mainFrame = Roact.createElement("ScrollingFrame", {
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
ScrollBarThickness = 5,
CanvasSize = UDim2.new(1, 0, 0, componentHeight),
BackgroundTransparency = 1,
}, elements),
})
end
return DataConsumer(DataStoresChart, "DataStoresData")
@@ -0,0 +1,35 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local DataStoresChart = require(script.Parent.DataStoresChart)
it("should create and destroy without errors", function()
local element = Roact.createElement(DataProvider, {}, {
DataStoresChart = Roact.createElement(DataStoresChart)
})
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
DataStoresChart = Roact.createElement(DataStoresChart)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,115 @@
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Signal = require(script.Parent.Parent.Parent.Signal)
local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount"))
local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator)
local DataStoresData = {}
DataStoresData.__index = DataStoresData
function DataStoresData.new()
local self = {}
setmetatable(self, DataStoresData)
self._dataStoresUpdated = Signal.new()
self._dataStoresData = {}
self._dataStoresDataCount = 0
self._lastUpdateTime = 0
self._isRunning = false
return self
end
function DataStoresData:Signal()
return self._dataStoresUpdated
end
function DataStoresData:getCurrentData()
return self._dataStoresData, self._dataStoresDataCount
end
function DataStoresData:updateValue(key, value)
if not self._dataStoresData[key] then
local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT)
newBuffer:push_back({
value = value,
time = self._lastUpdateTime
})
self._dataStoresData[key] = {
max = value,
min = value,
dataSet = newBuffer,
}
else
local dataEntry = self._dataStoresData[key]
local currMax = dataEntry.max
local currMin = dataEntry.min
local update = {
value = value,
time = self._lastUpdateTime
}
local overwrittenEntry = self._dataStoresData[key].dataSet:push_back(update)
if overwrittenEntry then
local iter = self._dataStoresData[key].dataSet:iterator()
local dat = iter:next()
if currMax == overwrittenEntry.value then
currMax = currMin
while dat do
currMax = dat.value < currMax and currMax or dat.value
dat = iter:next()
end
end
if currMin == overwrittenEntry.value then
currMin = currMax
while dat do
currMin = currMin < dat.value and currMin or dat.value
dat = iter:next()
end
end
end
self._dataStoresData[key].max = currMax < value and value or currMax
self._dataStoresData[key].min = currMin < value and currMin or value
end
end
function DataStoresData:isRunning()
return self._isRunning
end
function DataStoresData:start()
local clientReplicator = getClientReplicator()
if clientReplicator and not self._statsListenerConnection then
self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats)
local dataStoreBudget = stats.DataStoreBudget
self._lastUpdateTime = os.time()
if dataStoreBudget then
local count = 0
for k, v in pairs(dataStoreBudget) do
if type(v) == 'number' then
self:updateValue(k,v)
count = count + 1
end
end
self._dataStoresDataCount = count
self._dataStoresUpdated:Fire(self._dataStoresData, self._dataStoresDataCount)
end
end)
clientReplicator:RequestServerStats(true)
self._isRunning = true
end
end
function DataStoresData:stop()
-- listeners are responsible for disconnecting themselves
if self._statsListenerConnection then
self._statsListenerConnection:Disconnect()
self._statsListenerConnection = nil
end
self._isRunning = false
end
return DataStoresData
@@ -0,0 +1,105 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local DataStoresChart = require(Components.DataStores.DataStoresChart)
local UtilAndTab = require(Components.UtilAndTab)
local Actions = script.Parent.Parent.Parent.Actions
local DataStoresUpdateSearchFilter = require(Actions.DataStoresUpdateSearchFilter)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MainViewDataStores = Roact.PureComponent:extend("MainViewDataStores")
function MainViewDataStores:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onSearchTermChanged = function(newSearchTerm)
self.props.dispatchDataStoresUpdateSearchFilter(newSearchTerm, {})
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0
}
end
function MainViewDataStores:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewDataStores:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewDataStores:render()
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local searchTerm = self.props.storesSearchTerm
local utilTabHeight = self.state.utilTabHeight
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, PADDING),
SortOrder = Enum.SortOrder.LayoutOrder,
}),
UtilAndTab = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onSearchTermChanged = self.onSearchTermChanged,
}),
DataStores = utilTabHeight > 0 and Roact.createElement(DataStoresChart, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
})
end
local function mapStateToProps(state, props)
return {
storesSearchTerm = state.DataStoresData.storesSearchTerm,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchDataStoresUpdateSearchFilter = function(searchTerm, filters)
dispatch(DataStoresUpdateSearchFilter(searchTerm, filters))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewDataStores)
@@ -0,0 +1,37 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewDataStores = require(script.Parent.MainViewDataStores)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
DataStoresData = {
storesSearchTerm = ""
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewDataStores = Roact.createElement(MainViewDataStores, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,203 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Constants = require(script.Parent.Parent.Constants)
local FRAME_HEIGHT = Constants.TopBarFormatting.FrameHeight
local ICON_SIZE = .5 * FRAME_HEIGHT
local ICON_PADDING = (FRAME_HEIGHT - ICON_SIZE) / 2
local UserInputService = game:GetService("UserInputService")
local DEVCONSOLE_TEXT = "Developer Console"
local DEVCONSOLE_TEXT_X_PADDING = 4
local DEVCONSOLE_TEXT_FRAMESIZE = TextService:GetTextSize(DEVCONSOLE_TEXT, Constants.DefaultFontSize.TopBar,
Constants.Font.TopBar, Vector2.new(0, 0))
local LiveUpdateElement = require(script.Parent.Parent.Components.LiveUpdateElement)
local SetDevConsolePosition = require(script.Parent.Parent.Actions.SetDevConsolePosition)
local DevConsoleTopBar = Roact.Component:extend("DevConsoleTopBar")
local FFlagDevConsoleIsVeryStickyWhyWillItNotLetGo = settings():GetFFlag("DevConsoleIsVeryStickyWhyWillItNotLetGo")
function DevConsoleTopBar:init()
self.inputBegan = function(rbx,input)
if self.props.isMinimized then
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local absPos = self.ref.current.AbsolutePosition
local startPos = Vector3.new(absPos.X, absPos.Y, 0)
self:setState({
startPos = startPos,
startOffset = input.Position,
moving = true,
})
if FFlagDevConsoleIsVeryStickyWhyWillItNotLetGo then
local inputEndedConn
local function onInputEnded(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self.inputEnded(nil, input)
inputEndedConn:Disconnect()
end
end
inputEndedConn = UserInputService.InputEnded:Connect(onInputEnded)
end
end
end
self.inputChanged = function(rbx,input)
if self.state.moving then
local offset = self.state.startPos - self.state.startOffset
offset = offset + input.Position
local position = UDim2.new(0, offset.X, 0, offset.Y)
self.props.dispatchSetDevConsolePosition(position)
end
end
self.inputEnded = function(rbx,input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self:setState({
moving = false,
})
end
end
self.ref = Roact.createRef()
end
function DevConsoleTopBar:render()
local isMinimized = self.props.isMinimized
local formFactor = self.props.formFactor
local onMinimizeClicked = self.props.onMinimizeClicked
local onMaximizeClicked = self.props.onMaximizeClicked
local onCloseClicked = self.props.onCloseClicked
local moving = self.state.moving
local elements = {}
elements["WindowTitle"] = Roact.createElement("TextLabel", {
Text = DEVCONSOLE_TEXT,
TextSize = Constants.DefaultFontSize.TopBar,
TextColor3 = Color3.new(1, 1, 1),
Font = Constants.Font.TopBar,
Size = UDim2.new(0, DEVCONSOLE_TEXT_FRAMESIZE.X, 0, FRAME_HEIGHT),
Position = UDim2.new(0, DEVCONSOLE_TEXT_X_PADDING, 0, 0),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
})
-- padding multiplied by two for the padding on the left and the right
local devConsoleTextSizeAndPadding = DEVCONSOLE_TEXT_FRAMESIZE.X + (DEVCONSOLE_TEXT_X_PADDING * 2)
local liveStatsModulePos = UDim2.new(0, devConsoleTextSizeAndPadding, 0, 0)
local liveStatsModuleSize = UDim2.new(1, -2 * devConsoleTextSizeAndPadding, 0, FRAME_HEIGHT)
if isMinimized then
liveStatsModulePos = UDim2.new(0, 0, 1, 0)
liveStatsModuleSize = UDim2.new(1, 0, 1, 0)
elseif self.ref.current then
liveStatsModuleSize = UDim2.new(
0,
self.ref.current.AbsoluteSize.X - (2 * DEVCONSOLE_TEXT_FRAMESIZE.X),
0,
FRAME_HEIGHT
)
end
local topBarLiveUpdate = self.props.topBarLiveUpdate
elements["LiveStatsModule"] = Roact.createElement(LiveUpdateElement, {
topBarLiveUpdate = topBarLiveUpdate,
size = liveStatsModuleSize,
position = liveStatsModulePos,
})
-- minimize and maximize buttons should only appear on desktop
if formFactor == Constants.FormFactor.Large then
if not isMinimized then
elements["MinButton"] = Roact.createElement("ImageButton", {
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
Position = UDim2.new(1, -2 * FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING),
BorderColor3 = Color3.new(1, 0, 0),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
Image = Constants.Image.Minimize,
[Roact.Event.Activated] = onMinimizeClicked,
})
else
elements["MaxButton"] = Roact.createElement("ImageButton", {
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
Position = UDim2.new(1, -2 * FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING),
BorderColor3 = Color3.new(0, 0, 1),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
Image = Constants.Image.Maximize,
[Roact.Event.Activated] = onMaximizeClicked,
})
end
end
elements["CloseButton"] = Roact.createElement("ImageButton", {
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
Position = UDim2.new(1, -FRAME_HEIGHT + ICON_PADDING, 0, ICON_PADDING),
BorderColor3 = Color3.new(0, 1, 0),
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
Image = Constants.Image.Close,
[Roact.Event.Activated] = onCloseClicked,
})
--[[ we do this to catch all inputchanged events
if we can handle LARGE distances of continuous MouseMovmement input events
for dragging then we might be able to remove the portal
]]--
elements["MovmentCatchAll"] = moving and Roact.createElement(Roact.Portal, {
target = RobloxGui,
}, {
InputCatcher = Roact.createElement("ScreenGui", {
OnTopOfCoreBlur = true,
}, {
GreyOutFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = Constants.Color.Black,
BackgroundTransparency = .99,
Active = true,
[Roact.Event.InputChanged] = self.inputChanged,
[Roact.Event.InputEnded] = self.inputEnded,
})
})
})
return Roact.createElement("ImageButton", {
Size = UDim2.new(1, 0, 0, FRAME_HEIGHT),
BackgroundColor3 = Constants.Color.Black,
BackgroundTransparency = .5,
AutoButtonColor = false,
LayoutOrder = 1,
[Roact.Ref] = self.ref,
[Roact.Event.InputBegan] = self.inputBegan,
}, elements)
end
local function mapDispatchToProps(dispatch)
return {
dispatchSetDevConsolePosition = function (size)
dispatch(SetDevConsolePosition(size))
end
}
end
return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(DevConsoleTopBar)
@@ -0,0 +1,37 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.DataProvider)
local DevConsoleTopBar = require(script.Parent.DevConsoleTopBar)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
TopBarLiveUpdate = {
LogWarningCount = 0,
LogErrorCount = 0
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, nil, {
DevConsoleTopBar = Roact.createElement(DevConsoleTopBar)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,295 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui").RobloxGui
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local setMouseVisibility = require(script.Parent.Parent.Util.setMouseVisibility)
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local DevConsole = script.Parent.Parent
local Constants = require(DevConsole.Constants)
local TOPBAR_HEIGHT = Constants.TopBarFormatting.FrameHeight
local ROW_PADDING = Constants.Padding.WindowPadding
local MIN_SIZE = Constants.MainWindowInit.MinSize
local Components = DevConsole.Components
local DevConsoleTopBar = require(Components.DevConsoleTopBar)
local Actions = script.Parent.Parent.Actions
local ChangeDevConsoleSize = require(Actions.ChangeDevConsoleSize)
local SetDevConsoleVisibility = require(Actions.SetDevConsoleVisibility)
local SetDevConsoleMinimized = require(Actions.SetDevConsoleMinimized)
local BORDER_SIZE = 16
local DevConsoleWindow = Roact.PureComponent:extend("DevConsoleWindow")
function DevConsoleWindow:onMinimizeClicked()
self.props.dispatchSetDevConsoleMinimized(true)
end
function DevConsoleWindow:onMaximizeClicked()
self.props.dispatchSetDevConsoleMinimized(false)
end
function DevConsoleWindow:onCloseClicked()
self.props.dispatchSetDevConsolVisibility(false)
end
function DevConsoleWindow:init()
self.setDevConsoleSize = function (self, topLeft, bottomRight)
local x = bottomRight.X - topLeft.X
local y = bottomRight.Y - topLeft.Y
x = x < MIN_SIZE.X and MIN_SIZE.X or x
y = y < MIN_SIZE.Y and MIN_SIZE.Y or y
self.props.dispatchChangeDevConsoleSize(UDim2.new(0, x, 0, y))
end
self.resizeInputBegan = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local inputChangedConn, inputEndedConn
local function onInputChanged(input)
local currPosition = self.ref.current.AbsolutePosition
local cornerPos = input.Position
self:setDevConsoleSize(currPosition, cornerPos)
end
local function onInputEnded(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
inputChangedConn:Disconnect()
inputEndedConn:Disconnect()
end
end
inputChangedConn = UserInputService.InputChanged:Connect(onInputChanged)
inputEndedConn = UserInputService.InputEnded:Connect(onInputEnded)
end
end
self.doGamepadMenuButton = function(input)
if self.props.isVisible then
local keyToListenTo = input.KeyCode == Enum.KeyCode.ButtonStart or input.KeyCode == Enum.KeyCode.Escape
if keyToListenTo then
self.props.dispatchSetDevConsolVisibility(false)
end
end
end
self.ref = Roact.createRef()
self.state = {
resizing = false,
}
end
function DevConsoleWindow:didMount()
-- we need to run this delay before grabbing the size of the frame
-- because the DevconsoleWindow is mounted before the ScreenGui
-- is resized to the correct screen size.
delay(0, function ()
if self.ref.current then
local absPos1 = self.ref.current.AbsolutePosition
local absPos2 = self.ref.current.ResizeButton.AbsolutePosition
self:setDevConsoleSize(absPos1, absPos2)
end
end)
setMouseVisibility(self.props.isVisible)
self.gamepadMenuListener = UserInputService.InputBegan:Connect(self.doGamepadMenuButton)
end
function DevConsoleWindow:willUnmount()
if GuiService.SelectedCoreObject == self.ref.current then
GuiService.SelectedCoreObject = nil
end
self.gamepadMenuListener:Disconnect()
end
function DevConsoleWindow:didUpdate(previousProps, previousState)
setMouseVisibility(self.props.isVisible)
if self.props.isMinimized and (GuiService.SelectedCoreObject == self.ref.current) then
GuiService.SelectedCoreObject = nil
elseif self.props.isVisible ~= previousProps.isVisible or
self.props.currTabIndex ~= previousProps.currTabIndex then
local inputTypeEnum = UserInputService:GetLastInputType()
local isGamepad = (Enum.UserInputType.Gamepad1 == inputTypeEnum)
if isGamepad and self.props.isVisible then
GuiService.SelectedCoreObject = self.ref.current
else
GuiService.SelectedCoreObject = nil
end
end
end
function DevConsoleWindow:render()
local isVisible = self.props.isVisible
local formFactor = self.props.formFactor
local isDeveloperView = self.props.isDeveloperView
local currTabIndex = self.props.currTabIndex
local tabList = self.props.tabList
local isMinimized = self.props.isMinimized
local pos = self.props.position
local size = self.props.size
local resizing = self.state.resizing
local windowSize = size
local windowPos = UDim2.new()
local elements = {}
local borderSizePixel = BORDER_SIZE
if formFactor ~= Constants.FormFactor.Large then
-- none desktop/Large are full screen devconsoles
local absSize = CoreGui.AbsoluteSize
size = UDim2.new(0, absSize.X, 0, absSize.Y)
pos = UDim2.new(0, 0, 0, 0)
windowPos = UDim2.new(0, 16, 0, 0)
windowSize = size + UDim2.new(0, -32, 0, 0)
borderSizePixel = 0
end
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
Padding = UDim.new(0, ROW_PADDING),
})
if isMinimized then
elements["TopBar"] = Roact.createElement(DevConsoleTopBar, {
LayoutOrder = 1,
formFactor = formFactor,
isMinimized = true,
onMinimizeClicked = function()
self:onMinimizeClicked()
end,
onMaximizeClicked = function()
self:onMaximizeClicked()
end,
onCloseClicked = function()
self:onCloseClicked()
end,
})
return Roact.createElement("Frame", {
Position = UDim2.new(1, -500, 1, -2 * TOPBAR_HEIGHT),
Size = UDim2.new(0, 500, 0, 2 * TOPBAR_HEIGHT),
BackgroundColor3 = Color3.new(0, 0, 0),
Transparency = Constants.MainWindowInit.Transparency,
Active = true,
AutoLocalize = false,
Visible = isVisible,
Selectable = true,
BorderColor3 = Constants.Color.BaseGray,
[Roact.Ref] = self.ref,
}, elements)
else
elements["TopBar"] = Roact.createElement(DevConsoleTopBar, {
LayoutOrder = 1,
formFactor = formFactor,
isMinimized = false,
onMinimizeClicked = function()
self:onMinimizeClicked()
end,
onMaximizeClicked = function()
self:onMaximizeClicked()
end,
onCloseClicked = function()
self:onCloseClicked()
end,
})
local mainViewSize = windowSize
local TopSectionHeight = TOPBAR_HEIGHT + 2 * ROW_PADDING
local mainViewSizeOffset = UDim2.new(0, 0, 0, TopSectionHeight)
mainViewSize = mainViewSize - mainViewSizeOffset
if self.ref.current and isVisible and tabList then
local targetTab = tabList[currTabIndex]
if targetTab then
elements["MainView"] = Roact.createElement( targetTab.tab, {
size = mainViewSize,
formFactor = formFactor,
isDeveloperView = isDeveloperView,
tabList = tabList,
})
end
end
return Roact.createElement("Frame", {
Position = pos,
Size = size,
Visible = isVisible,
BackgroundColor3 = Color3.new(0, 0, 0),
Transparency = Constants.MainWindowInit.Transparency,
BorderColor3 = Constants.Color.BaseGray,
BorderSizePixel = borderSizePixel,
Active = true,
AutoLocalize = false,
Selectable = false,
[Roact.Ref] = self.ref,
}, {
DevConsoleUI = Roact.createElement("Frame", {
Size = windowSize,
Position = windowPos,
BackgroundTransparency = 1,
},elements),
ResizeButton = Roact.createElement("ImageButton", {
Position = UDim2.new(1, 0, 1, 0),
Size = UDim2.new(0, borderSizePixel, 0, borderSizePixel),
BackgroundColor3 = Color3.new(0, 0, 0),
Modal = true,
[Roact.Event.InputBegan] = self.resizeInputBegan,
}),
})
end
end
local function mapStateToProps(state, props)
return {
isVisible = state.DisplayOptions.isVisible,
isMinimized = state.DisplayOptions.isMinimized,
position = state.DisplayOptions.position,
size = state.DisplayOptions.size,
isDeveloperView = state.MainView.isDeveloperView,
currTabIndex = state.MainView.currTabIndex,
tabList = state.MainView.tabList,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchChangeDevConsoleSize = function (size)
dispatch(ChangeDevConsoleSize(size))
end,
dispatchSetDevConsolVisibility = function (isVisible)
dispatch(SetDevConsoleVisibility(isVisible))
end,
dispatchSetDevConsoleMinimized = function (isMinimized)
dispatch(SetDevConsoleMinimized(isMinimized))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(DevConsoleWindow)
@@ -0,0 +1,52 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.DataProvider)
local DevConsoleWindow = require(script.Parent.DevConsoleWindow)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
DisplayOptions = {
isVisible = 0,
platform = 0,
isMinimized = false,
size = UDim2.new(1, 0, 1, 0),
},
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
TopBarLiveUpdate = {
LogWarningCount = 0,
LogErrorCount = 0
},
LogData = {
clientData = { },
clientDataFiltered = { },
clientSearchTerm = "",
clientTypeFilters = { },
serverData = { },
serverDataFiltered = { },
serverSearchTerm = "",
serverTypeFilters = { },
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
DevConsoleWindow = Roact.createElement(DevConsoleWindow)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,153 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local FONT = Constants.Font.UtilBar
local FONT_SIZE = Constants.DefaultFontSize.UtilBar
local ARROW_SIZE = Constants.GeneralFormatting.DropDownArrowHeight
local ARROW_OFFSET = ARROW_SIZE / 2
local OPEN_ARROW = Constants.Image.DownArrow
local INNER_FRAME_PADDING = 12
local DropDown = Roact.Component:extend("DropDown")
function DropDown:init()
self.onMainButtonPressed = function(rbx, input)
self:setState({
showDropDown = true,
})
end
self.nonDropDownSelection = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
showDropDown = false
})
end
end
self.state = {
showDropDown = false,
}
self.ref = Roact.createRef()
end
function DropDown:render()
local buttonSize = self.props.buttonSize
local dropDownList = self.props.dropDownList
local selectedIndex = self.props.selectedIndex
local onSelection = self.props.onSelection
local layoutOrder = self.props.layoutOrder
local dropDownTargetParent = self.props.dropDownTargetParent
local showDropDown = self.ref.current and self.state.showDropDown
local children = {}
local absolutePosition
local outerFrameSize
local frameHeight = 0
local frameWidth = 0
if self.ref.current and showDropDown then
local absolutePos = self.ref.current.AbsolutePosition
local absoluteSize = self.ref.current.AbsoluteSize
frameWidth = absoluteSize.X
children["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
})
for ind, name in pairs(dropDownList) do
local color = (ind == selectedIndex) and Constants.Color.SelectedGray or Constants.Color.UnselectedGray
children[name] = Roact.createElement("TextButton", {
Size = buttonSize,
Text = name,
TextColor3 = Constants.Color.Text,
TextSize = FONT_SIZE,
Font = FONT,
AutoButtonColor = false,
BackgroundColor3 = color,
BackgroundTransparency = 0,
BorderSizePixel = 0,
LayoutOrder = ind,
[Roact.Event.Activated] = function()
onSelection(ind)
self:setState({
showDropDown = false
})
end
})
frameHeight = frameHeight + absoluteSize.Y
end
local padding = 2 * INNER_FRAME_PADDING
outerFrameSize = UDim2.new(0, frameWidth + padding, 0, frameHeight + padding)
absolutePosition = UDim2.new(0, absolutePos.X, 0, absolutePos.Y + absoluteSize.Y)
end
return Roact.createElement("TextButton", {
Size = buttonSize,
Text = dropDownList[selectedIndex],
TextColor3 = Constants.Color.Text,
TextSize = FONT_SIZE,
Font = FONT,
AutoButtonColor = false,
BackgroundColor3 = Constants.Color.UnselectedGray,
BackgroundTransparency = 0,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = self.onMainButtonPressed,
[Roact.Ref] = self.ref,
}, {
arrow = Roact.createElement("ImageLabel", {
Image = OPEN_ARROW,
BackgroundTransparency = 1,
Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE),
Position = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, .5, -ARROW_OFFSET),
}),
DropDown = showDropDown and Roact.createElement(Roact.Portal, {
target = dropDownTargetParent ~= nil and dropDownTargetParent or game:GetService("CoreGui").DevConsoleMaster,
}, {
InputCatcher = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
[Roact.Event.InputEnded] = self.nonDropDownSelection,
}, {
OuterFrame = Roact.createElement("ImageButton", {
Size = outerFrameSize,
AutoButtonColor = false,
Position = absolutePosition,
BackgroundColor3 = Constants.Color.TextBoxGray,
BackgroundTransparency = 0,
}, {
innerFrame = Roact.createElement("Frame", {
Position = UDim2.new(0, INNER_FRAME_PADDING, 0 , INNER_FRAME_PADDING),
Size = UDim2.new(0, frameWidth, 0, frameHeight),
BackgroundTransparency = 1,
}, children)
})
})
})
})
end
return DropDown
@@ -0,0 +1,15 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DropDown = require(script.Parent.DropDown)
it("should create and destroy without errors", function()
local element = Roact.createElement(DropDown, {
dropDownList = {},
dropDownTargetParent = Instance.new("ScreenGui"),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,152 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local ENTRY_HEIGHT = Constants.GeneralFormatting.DropDownEntryHeight
local ARROW_SIZE = Constants.GeneralFormatting.DropDownArrowHeight
local ARROW_OFFSET = ARROW_SIZE / 2
local OPEN_ARROW = Constants.Image.DownArrow
local FULL_SCREEN_WIDTH = 375
local INNER_Y_OFFSET = 8
local INNER_X_OFFSET = 15
local FullScreenDropDownButton = Roact.Component:extend("FullScreenDropDownButton")
function FullScreenDropDownButton:init()
self.startDropDownView = function()
self:setState({
selectionScreenExpanded = true
})
end
self.noSelection = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or
(input.UserInputType == Enum.UserInputType.Touch and
input.UserInputState == Enum.UserInputState.End) then
self:setState({
selectionScreenExpanded = false
})
end
end
self.state = {
selectionScreenExpanded = false,
}
end
function FullScreenDropDownButton:render()
local buttonSize = self.props.buttonSize
local dropDownList = self.props.dropDownList
local selectedIndex = self.props.selectedIndex
local onSelection = self.props.onSelection
local layoutOrder = self.props.layoutOrder
local isSelecting = self.state.selectionScreenExpanded
local portalTarget = self.props.portalTarget
local dropDownItemList = {}
local scrollingFrameHeight = 2 * INNER_Y_OFFSET
if isSelecting then
dropDownItemList["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
})
if dropDownList then
for ind,name in ipairs(dropDownList) do
local color = (ind == selectedIndex) and Constants.Color.SelectedGray or Constants.Color.UnselectedGray
dropDownItemList[ind] = Roact.createElement("TextButton", {
Text = name,
Font = Constants.Font.TabBar,
TextSize = Constants.DefaultFontSize.DropDownTabBar,
TextColor3 = Constants.Color.Text,
AutoButtonColor = false,
Size = UDim2.new(1, 0, 0, ENTRY_HEIGHT),
BackgroundColor3 = color,
LayoutOrder = ind,
BorderSizePixel = 0,
[Roact.Event.Activated] = function(rbx)
self:setState({
selectionScreenExpanded = false
})
onSelection(ind)
end,
})
scrollingFrameHeight = scrollingFrameHeight + ENTRY_HEIGHT
end
end
end
return Roact.createElement("TextButton", {
Size = buttonSize,
BackgroundColor3 = Constants.Color.UnselectedGray,
Text = "",
AutoButtonColor = false,
LayoutOrder = layoutOrder,
[Roact.Event.Activated] = self.startDropDownView,
}, {
text = Roact.createElement("TextLabel", {
Size = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, 1, 0),
Text = dropDownList[selectedIndex],
Font = Constants.Font.TabBar,
TextSize = Constants.DefaultFontSize.DropDownTabBar,
TextXAlignment = Enum.TextXAlignment.Center,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
}),
arrow = Roact.createElement("ImageLabel", {
Image = OPEN_ARROW,
BackgroundTransparency = 1,
Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE),
Position = UDim2.new(1, -ARROW_SIZE - ARROW_OFFSET, .5, -ARROW_OFFSET),
}),
selectionView = isSelecting and Roact.createElement(Roact.Portal, {
target = portalTarget ~= nil and portalTarget or game:GetService("CoreGui").DevConsoleMaster,
}, {
GreyOutFrame = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = Constants.Color.Black,
BackgroundTransparency = .36,
Active = true,
[Roact.Event.InputEnded] = self.noSelection,
}, {
BorderFrame = Roact.createElement("Frame", {
Size = UDim2.new(0, FULL_SCREEN_WIDTH, 0, scrollingFrameHeight),
Position = UDim2.new(.5, -FULL_SCREEN_WIDTH / 2, 0, 0),
BackgroundColor3 = Constants.Color.UnselectedGray,
BorderSizePixel = 0,
}, {
SelectionFrame = Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, -2 * INNER_X_OFFSET, 1, -2 * INNER_Y_OFFSET),
Position = UDim2.new(0, INNER_X_OFFSET, 0, INNER_Y_OFFSET),
BackgroundTransparency = 1,
-- adding an extra entry's worth of height for easier access to last
-- child when trying to select last child
CanvasSize = UDim2.new(1, -2 * INNER_X_OFFSET, 1, ENTRY_HEIGHT),
BorderSizePixel = 0,
ScrollBarThickness = 0,
}, dropDownItemList)
})
})
})
})
end
return FullScreenDropDownButton
@@ -0,0 +1,15 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local FullScreenDropDownButton = require(script.Parent.FullScreenDropDownButton)
it("should create and destroy without errors", function()
local element = Roact.createElement(FullScreenDropDownButton, {
dropDownList = {},
portalTarget = Instance.new("ScreenGui"),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,34 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local FONT = Constants.Font.MainWindowHeader
local TEXT_SIZE = Constants.DefaultFontSize.MainWindowHeader
local TEXT_COLOR = Constants.Color.Text
local function HeaderButton(props)
local text = props.text
local size = props.size
local pos = props.pos
local sortfunction = props.sortfunction
return Roact.createElement("TextButton", {
Text = text,
TextSize = TEXT_SIZE,
TextColor3 = TEXT_COLOR,
Font = FONT,
TextXAlignment = Enum.TextXAlignment.Left,
Size = size,
Position = pos,
BackgroundTransparency = 1,
[Roact.Event.Activated] = function()
if sortfunction then
sortfunction(text)
end
end,
})
end
return HeaderButton
@@ -0,0 +1,305 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LineGraphHoverDisplay = require(script.Parent.LineGraphHoverDisplay)
local Constants = require(script.Parent.Parent.Constants)
local TEXT_COLOR = Constants.Color.Text
local MAIN_LINE_COLOR = Constants.Color.HighlightBlue
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local POINT_WIDTH = Constants.Graph.PointWidth
local POINT_OFFSET = Constants.Graph.PointOffset
local GRAPH_PADDING = Constants.Graph.Padding
local GRAPH_SCALE = Constants.Graph.Scale
local GRAPH_Y_INNER_PADDING = Constants.Graph.InnerPaddingY
local GRAPH_Y_INNER_SCALE = Constants.Graph.InnerScaleY
local TEXT_PADDING = Constants.Graph.TextPadding
local INVIS_LINE_THRESHOLD = 10
local LineGraph = Roact.Component:extend("LineGraph")
function LineGraph:init()
self.onGraphInputChanged = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
if not self.state.holdPos then
self:setState({
inputPosition = input.Position,
})
end
end
end
self.onGraphInputEnded = function(rbx, input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
if not self.state.holdPos then
self:setState({
inputPosition = false
})
end
elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
self:setState({
holdPos = not self.state.holdPos
})
end
end
self.graphRef = Roact.createRef()
self.state = {
selectedTimeStamps = {}
}
end
function LineGraph:didUpdate()
if self.state.absGraphSize ~= self.graphRef.current.AbsoluteSize then
local absSize = self.graphRef.current.AbsoluteSize
local absPos = self.graphRef.current.AbsolutePosition
self:setState({
absGraphSize = absSize,
absGraphPos = absPos,
})
end
end
function LineGraph:didMount()
local absSize = self.graphRef.current.AbsoluteSize
local absPos = self.graphRef.current.AbsolutePosition
self:setState({
absGraphSize = absSize,
absGraphPos = absPos,
})
end
function LineGraph:render()
local size = self.props.size
local pos = self.props.pos
local graphData = self.props.graphData
local getX = self.props.getX
local getY = self.props.getY
local stringFormatX = self.props.stringFormatX
local stringFormatY = self.props.stringFormatY
local axisLabelX = self.props.axisLabelX
local axisLabelY = self.props.axisLabelY
local layoutOrder = self.props.layoutOrder
local inputPosition = self.state.inputPosition
local absGraphSize = self.state.absGraphSize
local absGraphPos = self.state.absGraphPos
local maxX = getX(graphData:back())
local minX = getX(graphData:front())
local maxY = self.props.maxY
local minY = self.props.minY
local hoverLineY
local elements = {}
if absGraphSize then
local dataPoints = {}
local dataIter = graphData:iterator()
local data = dataIter:next()
while data do
local datapoint = getY(data)
local time = getX(data)
local xdivisor = maxX - minX
local xPosition = xdivisor > 0 and (time - minX) / xdivisor or 0
local ydivisor = maxY - minY
local yPosition = ydivisor > 0 and (datapoint - minY) / ydivisor or 1
local point = {
X = xPosition,
Y = yPosition,
data = data,
}
table.insert(dataPoints, point)
data = dataIter:next()
end
for i = 2, #dataPoints do
local aX = dataPoints[i].X * absGraphSize.X
local aY = dataPoints[i].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE
local bX = dataPoints[i - 1].X * absGraphSize.X
local bY = dataPoints[i - 1].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE
if aX ~= bX then
local vecPosX = (aX + bX) / 2
local vecPosY = (aY + bY) / 2
local vecX = aX - bX
local vecY = aY - bY
local length = math.sqrt((vecX * vecX) + (vecY * vecY))
local rot = math.deg(math.atan2(vecY, vecX))
table.insert(elements, Roact.createElement("Frame", {
Size = UDim2.new(0, length, 0, LINE_WIDTH),
Position = UDim2.new(0, vecPosX - length / 2, 1 - GRAPH_Y_INNER_PADDING, -vecPosY),
BackgroundColor3 = MAIN_LINE_COLOR,
BorderSizePixel = 0,
Rotation = -rot,
}))
table.insert(elements, Roact.createElement("Frame", {
Size = UDim2.new(0, POINT_WIDTH, 0, POINT_WIDTH),
Position = UDim2.new(0, aX, 1 - GRAPH_Y_INNER_PADDING, -aY - POINT_OFFSET),
BackgroundColor3 = MAIN_LINE_COLOR,
BorderSizePixel = 0,
}))
if inputPosition then
local hoverLineX = inputPosition.X - absGraphPos.X
if hoverLineX < aX and bX < hoverLineX then
local aDataX = getX(dataPoints[i].data)
local bDataX = getX(dataPoints[i - 1].data)
local aDataY = getY(dataPoints[i].data)
local bDataY = getY(dataPoints[i - 1].data)
local ratio = (hoverLineX - bX) / vecX
hoverLineY = bY + (vecY * ratio)
local hoverValX = (aDataX - bDataX) * ratio + bDataX
local hoverValY = (aDataY - bDataY) * ratio + bDataY
elements["HoverDetails"] = Roact.createElement(LineGraphHoverDisplay, {
hoverLineX = hoverLineX,
hoverLineY = hoverLineY,
hoverValX = hoverValX,
hoverValY = hoverValY,
stringFormatX = stringFormatX,
stringFormatY = stringFormatY,
})
end
end
end
end
if #dataPoints > 0 then
local lastEntryHeight = dataPoints[#dataPoints].Y * absGraphSize.Y * GRAPH_Y_INNER_SCALE
local currValue = getY(dataPoints[#dataPoints].data)
-- calc if the hovered Y position is very close to the last input entry (within the invis-threshold).
-- If it's NOT within the "invis threshold" then we can show the line and "last entry value"
local showCurrValue = not (hoverLineY and math.abs(lastEntryHeight - hoverLineY) < INVIS_LINE_THRESHOLD)
-- calc if the hovered Y position is very close to the lower Y bound value (within the invis-threshold).
-- If it's NOT within the "invis threshold" then we can show the lowerbound Y value
local hoverLineCheck = (hoverLineY and math.abs(hoverLineY) < INVIS_LINE_THRESHOLD)
local showLeastValue = not (hoverLineCheck or math.abs(lastEntryHeight) < INVIS_LINE_THRESHOLD)
if showCurrValue then
elements["LatestEntryLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, TEXT_PADDING, 0, LINE_WIDTH),
Position = UDim2.new(0, -TEXT_PADDING, 1 - GRAPH_Y_INNER_PADDING, -lastEntryHeight),
BackgroundColor3 = LINE_COLOR,
BackgroundTransparency = .5,
BorderSizePixel = 0,
})
elements["LatestEntryText"] = Roact.createElement("TextLabel", {
Text = stringFormatY and stringFormatY(currValue) or currValue,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING, -lastEntryHeight),
BackgroundTransparency = 1,
})
end
if showLeastValue then
elements["AxisTextY0"] = Roact.createElement("TextLabel", {
Text = stringFormatY and stringFormatY(minY) or minY,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING,0),
BackgroundTransparency = 1,
})
end
elements["AxisX"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
elements["AxisY"] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
end
local axisTextPadding = 2 * TEXT_PADDING + 2
return Roact.createElement("Frame", {
Size = size,
Position = pos,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
name = Roact.createElement("TextLabel", {
Text = axisLabelY,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING, -TEXT_PADDING),
BackgroundTransparency = 1,
}),
minX = Roact.createElement("TextLabel", {
Text = stringFormatX and stringFormatX(minX) or minX,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING + GRAPH_SCALE, axisTextPadding),
BackgroundTransparency = 1,
}),
maxX = Roact.createElement("TextLabel", {
Text = stringFormatX and stringFormatX(maxX) or maxX,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
Position = UDim2.new(GRAPH_PADDING + GRAPH_SCALE, 0, GRAPH_PADDING + GRAPH_SCALE, axisTextPadding),
BackgroundTransparency = 1,
}),
axisLabelX = Roact.createElement("TextLabel", {
Text = axisLabelX,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
-- adding 2 to padding to push the label away from the axis line
Position = UDim2.new(.5, 0, GRAPH_PADDING + GRAPH_SCALE, axisTextPadding),
BackgroundTransparency = 1,
}),
graph = Roact.createElement("Frame", {
Position = UDim2.new(GRAPH_PADDING, 0, GRAPH_PADDING, 0),
Size = UDim2.new(GRAPH_SCALE, 0, GRAPH_SCALE, 0),
BackgroundTransparency = 1,
[Roact.Ref] = self.graphRef,
[Roact.Event.InputChanged] = self.onGraphInputChanged,
[Roact.Event.InputEnded] = self.onGraphInputEnded,
}, elements)
})
end
return LineGraph
@@ -0,0 +1,56 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Constants)
local HOVER_LINE_COLOR = Constants.Color.HoverGreen
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local TEXT_PADDING = Constants.Graph.TextPadding
local GRAPH_Y_INNER_PADDING = Constants.Graph.InnerPaddingY
return function(props)
local hoverLineX = props.hoverLineX
local hoverLineY = props.hoverLineY
local hoverValX = props.hoverValX
local hoverValY = props.hoverValY
local stringFormatX = props.stringFormatX
local stringFormatY = props.stringFormatY
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
hoverLine = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = UDim2.new(0, hoverLineX, 0, 0),
BackgroundColor3 = HOVER_LINE_COLOR,
BorderSizePixel = 0,
}),
HoverHorizontal = Roact.createElement("Frame", {
Size = UDim2.new(0, hoverLineX + TEXT_PADDING, 0, LINE_WIDTH),
Position = UDim2.new(0, -TEXT_PADDING, 1 - GRAPH_Y_INNER_PADDING, -hoverLineY),
BackgroundColor3 = HOVER_LINE_COLOR,
BorderSizePixel = 0,
}),
HoverTextY = Roact.createElement("TextLabel", {
Text = stringFormatY and stringFormatY(hoverValY) or hoverValY,
TextColor3 = HOVER_LINE_COLOR,
TextXAlignment = Enum.TextXAlignment.Right,
Position = UDim2.new(0, -TEXT_PADDING - 2, 1 - GRAPH_Y_INNER_PADDING, -hoverLineY),
BackgroundTransparency = 1,
}),
HoverTextX = Roact.createElement("TextLabel", {
Text = stringFormatX and stringFormatX(hoverValX) or hoverValX,
TextColor3 = HOVER_LINE_COLOR,
TextXAlignment = Enum.TextXAlignment.Center,
Position = UDim2.new(0, hoverLineX, 1, TEXT_PADDING),
BackgroundTransparency = 1,
}),
})
end
@@ -0,0 +1,319 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local DataConsumer = require(script.Parent.Parent.Components.DataConsumer)
local Actions = script.Parent.Parent.Actions
local SetActiveTab = require(Actions.SetActiveTab)
local Constants = require(script.Parent.Parent.Constants)
local MsgTypeNamesOrdered = Constants.MsgTypeNamesOrdered
local TOP_BAR_FONT_SIZE = Constants.DefaultFontSize.TopBar
local TEXT_COLOR = Constants.Color.Text
local FONT = Constants.Font.TopBar
local IMAGE_SIZE = UDim2.new(0, TOP_BAR_FONT_SIZE, 0, TOP_BAR_FONT_SIZE)
local MEM_STAT_STR_SMALL = "Client Mem:"
local memStatStrSmallWidth = TextService:GetTextSize(MEM_STAT_STR_SMALL, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0))
local MEM_STAT_STR = "Client Memory Usage:"
local memStatStrWidth = TextService:GetTextSize(MEM_STAT_STR, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0))
local AVG_PING_STR = "Avg. Ping:"
local avgPingStrWidth = TextService:GetTextSize(AVG_PING_STR, TOP_BAR_FONT_SIZE, FONT, Vector2.new(0, 0))
-- supposed to be the calculated width of the frame, but
-- doing this for now due to time constraints.
local MIN_LARGE_FORMFACTOR_WIDTH = 380
local INNER_PADDING = 6
local LiveUpdateElement = Roact.PureComponent:extend("LiveUpdateElement")
function LiveUpdateElement:didMount()
local totalMemSignal = self.props.ClientMemoryData:totalMemSignal()
self.totalMemConnector = totalMemSignal:Connect(function(totalClientMemory)
self:setState({totalClientMemory = totalClientMemory})
end)
self.avgPingConnector = self.props.ServerStatsData:avgPing():Connect(function(averagePing)
self:setState({averagePing = averagePing})
end)
self.logWarningErrorConnector = self.props.ClientLogData:errorWarningSignal():Connect(function(error, warning)
self:setState({
numErrors = error,
numWarnings = warning,
})
end)
self:doSizeCheck()
end
function LiveUpdateElement:didUpdate()
self:doSizeCheck()
end
function LiveUpdateElement:doSizeCheck()
if self.ref.current then
local formFactorThreshold = self.state.formFactorThreshold
local isSmallerThanFormFactorThreshold = self.ref.current.AbsoluteSize.X < formFactorThreshold
if isSmallerThanFormFactorThreshold ~= self.state.isSmallerThanFormFactorThreshold then
self:setState({
isSmallerThanFormFactorThreshold = isSmallerThanFormFactorThreshold
})
end
end
end
function LiveUpdateElement:willUnmount()
self.totalMemConnector:Disconnect()
self.totalMemConnector = nil
self.avgPingConnector:Disconnect()
self.avgPingConnector = nil
self.logWarningErrorConnector:Disconnect()
self.logWarningErrorConnector = nil
end
function LiveUpdateElement:init()
local errorInit, warningInit = self.props.ClientLogData:getErrorWarningCount()
self.onLogWarningButton = function()
local warningFilters = {}
for _, name in pairs(MsgTypeNamesOrdered) do
warningFilters[name] = false
end
warningFilters["Warning"] = true
self.props.ClientLogData:setFilters(warningFilters)
self.props.dispatchChangeTabClientLog()
end
self.onLogErrorButton = function()
local errorFilters = {}
for _, name in pairs(MsgTypeNamesOrdered) do
errorFilters[name] = false
end
errorFilters["Error"] = true
self.props.ClientLogData:setFilters(errorFilters)
self.props.dispatchChangeTabClientLog()
end
self.ref = Roact.createRef()
self.state = {
numErrors = errorInit,
numWarnings = warningInit,
totalClientMemory = 0,
averagePing = 0,
formFactorThreshold = MIN_LARGE_FORMFACTOR_WIDTH,
isSmallerThanFormFactorThreshold = false,
}
end
function LiveUpdateElement:render()
local size = self.props.size
local position = self.props.position
local formFactor = self.props.formFactor
local numErrors = self.state.numErrors
local numWarnings = self.state.numWarnings
local clientMemoryUsage = self.state.totalClientMemory
local averagePing = self.state.averagePing
local isSmallerThanFormFactorThreshold = self.state.isSmallerThanFormFactorThreshold
local useSmallForm = false
local currMemStrWidth = memStatStrWidth.X
local alignment = Enum.HorizontalAlignment.Center
if formFactor == Constants.FormFactor.Small or isSmallerThanFormFactorThreshold then
position = position + UDim2.new(0, INNER_PADDING * 2, 0, 0)
currMemStrWidth = memStatStrSmallWidth.X
useSmallForm = true
alignment = Enum.HorizontalAlignment.Left
end
local logErrorStat = string.format("%d", numErrors)
local logErrorStatVector = TextService:GetTextSize(
logErrorStat,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
local logWarningStat = string.format("%d", numWarnings)
local logWarningStatVector = TextService:GetTextSize(
logWarningStat,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
local memUsageString = string.format("%d MB", clientMemoryUsage)
local memUsageStringVector = TextService:GetTextSize(
memUsageString,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
local avgPingString = string.format("%d ms", averagePing)
local avgPingStringVector = TextService:GetTextSize(avgPingString,
TOP_BAR_FONT_SIZE,
FONT,
Vector2.new(0, 0)
)
if formFactor == Constants.FormFactor.Small or isSmallerThanFormFactorThreshold then
position = position + UDim2.new(0, INNER_PADDING * 2, 0, 0)
currMemStrWidth = memStatStrSmallWidth.X
useSmallForm = true
alignment = Enum.HorizontalAlignment.Left
end
local showNetworkPing = averagePing > 0
return Roact.createElement("Frame", {
Position = position,
Size = size,
BackgroundTransparency = 1,
[Roact.Ref] = self.ref,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, INNER_PADDING),
HorizontalAlignment = alignment,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
LogErrorIcon = Roact.createElement("ImageButton", {
Image = Constants.Image.Error,
Size = IMAGE_SIZE,
BackgroundTransparency = 1,
LayoutOrder = 1,
[Roact.Event.Activated] = self.onLogErrorButton,
}),
LogErrorCount = Roact.createElement("TextButton", {
Text = logErrorStat,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, logErrorStatVector.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
[Roact.Event.Activated] = self.onLogErrorButton,
}),
ErrorWarningPad = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 3,
}),
LogWarningIcon = Roact.createElement("ImageButton", {
Image = Constants.Image.Warning,
Size = IMAGE_SIZE,
BackgroundTransparency = 1,
LayoutOrder = 4,
[Roact.Event.Activated] = self.onLogWarningButton,
}),
LogWarningCount = Roact.createElement("TextButton", {
Text = logWarningStat,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, logWarningStatVector.X, 1, 0),
BackgroundTransparency = 9,
LayoutOrder = 5,
[Roact.Event.Activated] = self.onLogWarningButton,
}),
WarningMemoryPad = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 6,
}),
MemoryUsage = Roact.createElement("TextButton", {
Text = useSmallForm and MEM_STAT_STR_SMALL or MEM_STAT_STR,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = Constants.Color.WarningYellow,
TextXAlignment = Enum.TextXAlignment.Right,
Font = FONT,
Size = UDim2.new(0, currMemStrWidth, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 7,
[Roact.Event.Activated] = self.props.dispatchChangeTabClientMemory,
}),
MemoryUsage_MB = Roact.createElement("TextButton", {
Text = memUsageString,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, memUsageStringVector.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 8,
[Roact.Event.Activated] = self.props.dispatchChangeTabClientMemory,
}),
MemoryPingPad = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 9,
}),
AvgPing = not useSmallForm and showNetworkPing and Roact.createElement("TextButton", {
Text = AVG_PING_STR,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = Constants.Color.WarningYellow,
TextXAlignment = Enum.TextXAlignment.Right,
Font = FONT,
Size = UDim2.new(0, avgPingStrWidth.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 10,
[Roact.Event.Activated] = self.props.dispatchChangeTabNetworkPing,
}),
AvgPing_ms = not useSmallForm and showNetworkPing and Roact.createElement("TextButton", {
Text = avgPingString,
TextSize = TOP_BAR_FONT_SIZE,
TextColor3 = TEXT_COLOR,
TextXAlignment = Enum.TextXAlignment.Left,
Font = FONT,
Size = UDim2.new(0, avgPingStringVector.X, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = 11,
[Roact.Event.Activated] = self.props.dispatchChangeTabNetworkPing,
})
})
end
local function mapDispatchToProps(dispatch)
return {
dispatchChangeTabClientLog = function()
dispatch(SetActiveTab("Log", true))
end,
dispatchChangeTabClientMemory = function()
dispatch(SetActiveTab("Memory", true))
end,
dispatchChangeTabNetworkPing = function()
dispatch(SetActiveTab("ServerStats", true))
end,
}
end
return RoactRodux.UNSTABLE_connect2(nil, mapDispatchToProps)(
DataConsumer(LiveUpdateElement, "ServerStatsData", "ClientMemoryData", "ClientLogData" )
)
@@ -0,0 +1,34 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.DataProvider)
local LiveUpdateElement = require(script.Parent.LiveUpdateElement)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, nil, {
LiveUpdateElement = Roact.createElement(LiveUpdateElement, {
size = UDim2.new(),
position = UDim2.new(),
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,23 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataConsumer = require(script.Parent.Parent.DataConsumer)
local LogOutput = require(script.Parent.LogOutput)
local ClientLog = Roact.Component:extend("ClientLog")
function ClientLog:init()
self.initClientLogData = function()
return self.props.ClientLogData:getLogData()
end
end
function ClientLog:render()
return Roact.createElement(LogOutput, {
layoutOrder = self.props.layoutOrder,
size = self.props.size,
initLogOutput = self.initClientLogData,
targetSignal = self.props.ClientLogData:Signal(),
})
end
return DataConsumer(ClientLog, "ClientLogData")
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local ClientLog = require(script.Parent.ClientLog)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
ClientLog = Roact.createElement(ClientLog)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,143 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local LogService = game:GetService("LogService")
local Roact = require(CorePackages.Roact)
local DataConsumer = require(script.Parent.Parent.DataConsumer)
local ScrollingTextBox = require(script.Parent.Parent.ScrollingTextBox)
local Constants = require(script.Parent.Parent.Parent.Constants)
local COMMANDLINE_INDENT = Constants.LogFormatting.CommandLineIndent
local COMMANDLINE_FONTSIZE = Constants.DefaultFontSize.CommandLine
local FONT = Constants.Font.Log
local DevConsoleCommandLine = Roact.PureComponent:extend("DevConsoleCommandLine")
function DevConsoleCommandLine:init()
self.onFocusLost = function(rbx, enterPressed, inputThatCausedFocusLoss)
if enterPressed then
LogService:ExecuteScript(rbx.Text)
rbx:CaptureFocus()
end
end
self.textbox = Roact.createRef()
end
function DevConsoleCommandLine:didMount()
if not self.onFocusConnection then
self.onFocusConnection = UserInputService.InputBegan:Connect(function(input)
if self.textbox.current and self.textbox.current:IsFocused() then
local rbx = self.textbox.current
local serverLogData = self.props.ServerLogData
local cmdHistory = serverLogData:getCommandLineHistory()
local cmdIndex = serverLogData:getCommandLineIndex()
if input.KeyCode == Enum.KeyCode.Up then
local newIndex = cmdIndex + 1
newIndex = math.min(cmdHistory:getSize(), newIndex)
cmdIndex = newIndex
rbx.Text = cmdHistory:reverseAt(newIndex) or ""
elseif input.KeyCode == Enum.KeyCode.Down then
local newIndex = cmdIndex - 1
newIndex = math.max(0, newIndex)
cmdIndex = newIndex
rbx.Text = cmdHistory:reverseAt(newIndex) or ""
elseif input.KeyCode == Enum.KeyCode.Return then
if #rbx.Text:gsub("%s+", "") > 0 then
local prevText = cmdHistory:reverseAt(1)
if prevText ~= rbx.Text then
cmdHistory:push_back(rbx.Text)
end
end
cmdIndex = 0
elseif cmdIndex ~= 0 then
cmdIndex = 0
end
serverLogData:setCommandLineIndex(cmdIndex)
end
end)
end
-- theres an unwanted behavior here
-- if you recapture the same textbox in the onFocuslost event,
-- the "\r" character that triggerd the onFocusLost will not be consumed yet
-- and will subsequently be put into the textbox. This event is meant to fix that.
if not self.fixUnwantedReturnCapture then
self.fixUnwantedReturnCapture = UserInputService.InputEnded:Connect(function(input)
if self.textbox.current and self.textbox.current:IsFocused() then
if input.KeyCode == Enum.KeyCode.Return then
self.textbox.current.Text = ""
end
end
end)
end
end
function DevConsoleCommandLine:willUnmount()
if self.onFocusConnection then
self.onFocusConnection:Disconnect()
self.onFocusConnection = nil
end
if self.fixUnwantedReturnCapture then
self.fixUnwantedReturnCapture:Disconnect()
self.fixUnwantedReturnCapture = nil
end
end
function DevConsoleCommandLine:render()
local height = self.props.height
local pos = self.props.pos
local initText = ""
local cmdIndex = self.props.ServerLogData:getCommandLineIndex()
if cmdIndex ~= 0 then
local cmdHistory = self.props.ServerLogData:getCommandLineHistory()
initText = cmdHistory:reverseAt(cmdIndex) or ""
end
return Roact.createElement("Frame", {
Position = pos,
Size = UDim2.new(1, 0, 0, height),
BackgroundTransparency = 0,
BackgroundColor3 = Constants.Color.TextBoxGray,
BorderColor3 = Constants.Color.BorderGray,
BorderSizePixel = 1,
}, {
Arrow = Roact.createElement("TextLabel", {
Size = UDim2.new(0, COMMANDLINE_INDENT, 1, 0),
BackgroundTransparency = 1,
TextSize = COMMANDLINE_FONTSIZE,
Font = FONT,
Text = "> ",
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Right,
}),
InputField = Roact.createElement(ScrollingTextBox, {
Position = UDim2.new(0, COMMANDLINE_INDENT, 0, 0),
Size = UDim2.new(1, -COMMANDLINE_INDENT, 0, height),
ShowNativeInput = true,
ClearTextOnFocus = false,
TextColor3 = Constants.Color.Text,
TextXAlignment = 0,
TextSize = COMMANDLINE_FONTSIZE,
Text = initText,
Font = FONT,
PlaceholderText = "command line",
[Roact.Ref] = self.textbox,
TextBoxFocusLost = self.onFocusLost,
})
})
end
return DataConsumer(DevConsoleCommandLine, "ServerLogData")
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local DevConsoleCommandLine = require(script.Parent.DevConsoleCommandLine)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
CmdLine = Roact.createElement(DevConsoleCommandLine),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,343 @@
local LogService = game:GetService("LogService")
local TextService = game:GetService("TextService")
local Constants = require(script.Parent.Parent.Parent.Constants)
local FONT_SIZE = Constants.DefaultFontSize.MainWindow
local FONT = Constants.Font.Log
local MAX_STRING_SIZE = Constants.LogFormatting.MaxStringSize
local MESSAGE_TO_TYPENAME = Constants.EnumToMsgTypeName
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Signal = require(script.Parent.Parent.Parent.Signal)
-- 500 is max kept in history in C++ as of 7/2/2018
local MAX_LOG_SIZE = tonumber(settings():GetFVariable("NewDevConsoleMaxLogCount"))
local WARNING_TO_FILTER = {"ClassDescriptor failed to learn", "EventDescriptor failed to learn", "Type failed to learn"}
local MAX_HISTORY = 100
local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp)
local LogData = {}
LogData.__index = LogData
local function messageEntry(msg, timeAsStr, type)
local fmtMessage
local charCount = #msg
if charCount < MAX_STRING_SIZE then
fmtMessage = string.format("%s -- %s", timeAsStr, msg)
else
fmtMessage = string.format("%s -- %s", timeAsStr, string.sub(msg, 1, MAX_STRING_SIZE))
end
local dims = TextService:GetTextSize(fmtMessage, FONT_SIZE, FONT, Vector2.new())
return {
Message = fmtMessage,
CharCount = charCount,
Type = type,
Dims = dims,
}
end
-- MOST if not all of this code is copied from the
-- Filter "ClassDescriptor failed to learn" errors
local function ignoreWarningMessageOnAdd(message)
if message.Type ~= Enum.MessageType.MessageWarning.Value then
return false
end
local found = false
for _, filterString in ipairs(WARNING_TO_FILTER) do
if string.find(message.Message, filterString) ~= nil then
found = true
break
end
end
return found
end
-- if a message if filtered that means we need to put it into the filtered messages
-- this is defined as the messages that we are searching for when the search
-- feature is being used
local function isMessageFiltered(message, filterTypes, filterTerm)
-- if any types are flagged on, then we need to check against it
if #filterTerm == 0 and not next(filterTypes) then
return false
end
if next(filterTypes) then
if not filterTypes[MESSAGE_TO_TYPENAME[message.Type]] then
return false
end
end
if #filterTerm > 0 then
if string.find(message.Message:lower(), filterTerm:lower()) == nil then
return false
end
end
return true
end
local function validActiveFilters(filterTypes)
local validFilters = false
for _, filterActive in pairs(filterTypes) do
validFilters = validFilters or filterActive
end
return validFilters
end
local function filterMessages(buffer, msgIter, filterTypes, filterTerm)
buffer:reset()
local validFilters = validActiveFilters(filterTypes)
if #filterTerm == 0 and not validFilters then
return
end
local counter = 0
local msg = msgIter:next()
while msg do
if isMessageFiltered(msg, filterTypes, filterTerm) then
counter = counter + 1
buffer:push_back(msg)
end
msg = msgIter:next()
end
if counter == 0 then
if #filterTerm > 0 then
local errorMsg = messageEntry(string.format ("\"%s\" was not found", filterTerm), "", 0)
buffer:push_back(errorMsg)
else
local errorMsg = messageEntry("No Messages were found", "", 0)
buffer:push_back(errorMsg)
end
end
end
function LogData:checkErrorWarningCounter(msgType)
if msgType == Enum.MessageType.MessageWarning.Value then
self._warningCount = self._warningCount + 1
self._errorWarningSignal:Fire(self._errorCount, self._warningCount)
elseif msgType == Enum.MessageType.MessageError.Value then
self._errorCount = self._errorCount + 1
self._errorWarningSignal:Fire(self._errorCount, self._warningCount)
end
end
function LogData.new(isClient)
local self = {}
setmetatable(self, LogData)
self._initialized = false
self._isRunning = false
self._isClient = isClient
self._logData = CircularBuffer.new(MAX_LOG_SIZE)
self._logDataSearched = CircularBuffer.new(MAX_LOG_SIZE)
self._searchTerm = ""
self._commandLineHistory = CircularBuffer.new(MAX_HISTORY)
self._commandLineIndex = 0
self._filters = {}
for _, v in pairs(MESSAGE_TO_TYPENAME) do
self._filters[v] = true
end
self._errorCount = isClient and 0
self._warningCount = isClient and 0
self._logDataUpdate = Signal.new()
self._errorWarningSignal = isClient and Signal.new()
self._filterUpdated = Signal.new()
return self
end
function LogData:Signal()
return self._logDataUpdate
end
function LogData:errorWarningSignal()
return self._errorWarningSignal
end
function LogData:filterUpdatedSignal()
return self._filterUpdated
end
function LogData:setSearchTerm(targetSearchTerm)
if self._searchTerm ~= targetSearchTerm then
self._searchTerm = targetSearchTerm
if self._searchTerm == "" then
self._logDataSearched:reset()
self._logDataUpdate:Fire(self._logData)
else
filterMessages(
self._logDataSearched,
self._logData:iterator(),
self._filters,
self._searchTerm
)
self._logDataUpdate:Fire(self._logDataSearched)
end
end
end
function LogData:getSearchTerm()
return self._searchTerm
end
function LogData:getCommandLineHistory()
return self._commandLineHistory
end
function LogData:getCommandLineIndex()
return self._commandLineIndex
end
function LogData:setCommandLineIndex(index)
self._commandLineIndex = index
end
function LogData:getFilters()
return self._filters
end
function LogData:setFilter(name, newState)
local oldState = self._filters[name]
self._filters[name] = newState
if not validActiveFilters(self._filters) then
self._filters[name] = oldState
return
end
filterMessages(
self._logDataSearched,
self._logData:iterator(),
self._filters,
self._searchTerm
)
self._logDataUpdate:Fire(self._logDataSearched)
self._filterUpdated:Fire()
end
function LogData:setFilters(filters)
self._filters = filters
if not validActiveFilters(filters) then
self._logDataSearched:reset()
self._logDataUpdate:Fire(self._logData)
return
end
filterMessages(
self._logDataSearched,
self._logData:iterator(),
self._filters,
self._searchTerm
)
self._logDataUpdate:Fire(self._logDataSearched)
self._filterUpdated:Fire()
end
function LogData:getLogData()
if #self._logDataSearched:getData() > 0 then
return self._logDataSearched
end
return self._logData
end
function LogData:getErrorWarningCount()
return self._errorCount, self._warningCount
end
function LogData:isRunning()
return self._initialized
end
function LogData:start()
if self._isClient then
if not self._initialized then
self._initialized = true
local Messages = {}
if #Messages == 0 then
local history = LogService:GetLogHistory()
for _, msg in ipairs(history) do
local message = messageEntry(
msg.message or "[DevConsole Error 1]",
convertTimeStamp(msg.timestamp),
msg.messageType.Value
)
if not ignoreWarningMessageOnAdd(message) then
self:checkErrorWarningCounter(msg.messageType.Value)
self._logData:push_back(message)
end
end
end
end
self._connection = LogService.MessageOut:connect(function(text, messageType)
local message = messageEntry(
text or "[DevConsole Error 2]",
convertTimeStamp(os.time()),
messageType.Value
)
if not ignoreWarningMessageOnAdd(message) then
self:checkErrorWarningCounter(messageType.Value)
self._logData:push_back(message)
if #self._logDataSearched:getData() > 0 then
if isMessageFiltered(message, self._filters, self._searchTerm) then
self._logDataSearched:push_back(message)
self._logDataUpdate:Fire(self._logDataSearched)
end
else
self._logDataUpdate:Fire(self._logData)
end
end
end)
else
self._connection = LogService.ServerMessageOut:connect(function(text, messageType, timestamp)
local message = messageEntry(
text or "[DevConsole Error 3]",
convertTimeStamp(timestamp),
messageType.Value
)
if not ignoreWarningMessageOnAdd(message) then
self._logData:push_back(message)
if #self._logDataSearched:getData() > 0 then
if isMessageFiltered(message, self._filters, self._searchTerm) then
self._logDataSearched:push_back(message)
self._logDataUpdate:Fire(self._logDataSearched)
end
else
self._logDataUpdate:Fire(self._logData)
end
end
end)
LogService:RequestServerOutput()
end
self._isRunning = true
end
function LogData:stop()
self._initialized = false
self._isRunning = false
if self._connection then
self._connection:Disconnect()
end
end
return LogData
@@ -0,0 +1,24 @@
return function()
local LogData = require(script.Parent.LogData)
it("should initialize either client or server", function()
local clientLogData = LogData.new(true)
local serverLogData = LogData.new(false)
expect(clientLogData).to.be.ok()
expect(serverLogData).to.be.ok()
end)
it("should get and set the filters", function()
local clientLogData = LogData.new(true)
local key = "Output"
local value = false
local filters = clientLogData:getFilters()
expect(filters[key]).to.equal(true)
clientLogData:setFilter(key, value)
filters = clientLogData:getFilters()
expect(filters[key]).to.equal(value)
end)
end
@@ -0,0 +1,263 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Parent.Constants)
local FONT_SIZE = Constants.DefaultFontSize.MainWindow
local FONT = Constants.Font.Log
local ICON_PADDING = Constants.LogFormatting.IconHeight
local ARROW_OFFSET = Constants.LogFormatting.TextFrameHeight -- reuse this value for offset needed for the "<" char
local LINE_PADDING = Constants.LogFormatting.TextFramePadding
local MAX_STRING_SIZE = Constants.LogFormatting.MaxStringSize
local MAX_STR_MSG = " -- Could not display entire %d character message because message exceeds max displayable length of %d"
local FFlagDevConsoleLogNewLineFix = settings():GetFFlag("DevConsoleLogNewLineFix")
local LogOutput = Roact.Component:extend("LogOutput")
function LogOutput:init(props)
local initLogOutput = props.initLogOutput and props.initLogOutput()
self.onCanvasChange = function()
local current = self.ref.current
if current then
local canvasPos = current.CanvasPosition
local absSize = current.AbsoluteSize
if self.state.canvasPos ~= canvasPos or
self.state.absSize ~= absSize then
local yTotal = current.CanvasPosition.Y + current.AbsoluteSize.Y
local yCanvasSize = current.CanvasSize.Y.Offset
local autoScroll = yTotal == yCanvasSize
self:setState({
canvasPos = canvasPos,
absSize = absSize,
autoScroll = autoScroll,
})
end
end
end
self.ref = Roact.createRef()
self.state = {
logData = initLogOutput,
absSize = Vector2.new(),
canvasPos = Vector2.new(),
autoScroll = true,
wordWrap = true,
}
end
function LogOutput:willUpdate(nextProps, nextState)
self._canvasSignal:Disconnect()
self._absSizeSignal:Disconnect()
end
function LogOutput:didUpdate()
self._canvasSignal = self.ref.current:GetPropertyChangedSignal("CanvasPosition"):Connect(self.onCanvasChange)
self._absSizeSignal = self.ref.current:GetPropertyChangedSignal("AbsoluteSize"):Connect(self.onCanvasChange)
if self.state.autoScroll then
local current = self.ref.current
if current then
local newPos = Vector2.new(current.CanvasPosition.X, self.ref.current.CanvasSize.Y.Offset + self.ref.current.AbsoluteSize.Y)
current.CanvasPosition = newPos
end
end
end
function LogOutput:didMount()
self.logConnector = self.props.targetSignal:Connect(function(data)
if not self.state.autoScroll and
data:getSize() == data:getMaxSize() then
local canvasPos = self.state.canvasPos
local canvasPosY = canvasPos.Y
local newestMsg = data:back()
if newestMsg then
local msgDimsY = newestMsg.Dims.Y
local frameWidth = self.state.absSize.X - ARROW_OFFSET
if self.state.wordWrap and frameWidth > 0 then
msgDimsY = newestMsg.Dims.Y * math.ceil(newestMsg.Dims.X / frameWidth)
end
canvasPosY = math.max(0, canvasPosY - msgDimsY - LINE_PADDING)
end
self:setState({
logData = data,
canvasPos = Vector2.new(canvasPos.X, canvasPosY),
})
else
self:setState({
logData = data
})
end
end)
self._canvasSignal = self.ref.current:GetPropertyChangedSignal("CanvasPosition"):Connect(self.onCanvasChange)
self._absSizeSignal = self.ref.current:GetPropertyChangedSignal("AbsoluteSize"):Connect(self.onCanvasChange)
--[[
in some cases, the absolute size is not valid at this point. But in the
case that it is, we want to update the absolute size here since the
absolute size was changed prior to the absSizeSignal being set up
--]]
local absSize = self.ref.current.AbsoluteSize
if absSize.Magnitude > 0 then
self:setState({
absSize = self.ref.current.AbsoluteSize,
})
end
end
function LogOutput:willUnmount()
self.logConnector:Disconnect()
self.logConnector = nil
end
function LogOutput:render()
local layoutOrder = self.props.layoutOrder
local size = self.props.size
local logData = self.state.logData
local absSize = self.state.absSize
local canvasPos = self.state.canvasPos
local wordWrap = self.state.wordWrap
local elements = {}
local messageCount = 1
local scrollingFrameHeight = 0
if self.ref.current and logData then
local frameWidth = absSize.X - ARROW_OFFSET
local paddingHeight = -1
local usedFrameSpace = 0
local msgIter = logData:iterator()
local message = msgIter:next()
while message do
local fmtMessage = message.Message
local charCount = message.CharCount
local msgDimsY = message.Dims.Y
if wordWrap and frameWidth > 0 then
if FFlagDevConsoleLogNewLineFix then
-- this fix doesn't fully solve the problem, but it does prevent the white space
-- error to 2 lines at most, which should be acceptable until we can refactor
-- this whole hot mess
-- one full message height per time the message width fits in the container
msgDimsY = message.Dims.Y * message.Dims.X / absSize.X
-- round to the nearest line height
msgDimsY = math.ceil(msgDimsY / FONT_SIZE) * FONT_SIZE
else
msgDimsY = message.Dims.Y * math.ceil(message.Dims.X / frameWidth)
end
end
messageCount = messageCount + 1
if scrollingFrameHeight + msgDimsY >= canvasPos.Y then
if usedFrameSpace < absSize.Y then
local color = Constants.Color.Text
local image = ""
if message.Type == Enum.MessageType.MessageOutput.Value then
color = Constants.Color.Text
elseif message.Type == Enum.MessageType.MessageInfo.Value then
color = Constants.Color.HighlightBlue
image = Constants.Image.Info
elseif message.Type == Enum.MessageType.MessageWarning.Value then
color = Constants.Color.WarningYellow
image = Constants.Image.Warning
elseif message.Type == Enum.MessageType.MessageError.Value then
color = Constants.Color.ErrorRed
image = Constants.Image.Errors
end
elements[messageCount] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, msgDimsY),
BackgroundTransparency = 1,
LayoutOrder = messageCount,
}, {
image = Roact.createElement("ImageLabel", {
Image = image,
Size = UDim2.new(0, ICON_PADDING , 0, ICON_PADDING),
Position = UDim2.new(0, ICON_PADDING / 4, .5, -ICON_PADDING / 2),
BackgroundTransparency = 1,
}),
msg = Roact.createElement("TextLabel", {
Text = fmtMessage,
TextColor3 = color,
TextSize = FONT_SIZE,
Font = FONT,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = wordWrap,
Size = FFlagDevConsoleLogNewLineFix and
-- flag true
UDim2.new(1, -ARROW_OFFSET, 0, msgDimsY) or
-- flag false
UDim2.new(1, 0, 0, msgDimsY),
Position = UDim2.new(0, ARROW_OFFSET, 0, 0),
BackgroundTransparency = 1,
})
})
end
if paddingHeight < 0 then
paddingHeight = scrollingFrameHeight
else
usedFrameSpace = usedFrameSpace + msgDimsY + LINE_PADDING
end
end
scrollingFrameHeight = scrollingFrameHeight + msgDimsY + LINE_PADDING
if charCount < MAX_STRING_SIZE then
message = msgIter:next()
else
local maxStrMsg = string.format(MAX_STR_MSG, charCount, MAX_STRING_SIZE)
message = {
Message = maxStrMsg,
CharCount = #maxStrMsg,
Type = message.Type,
Dims = TextService:GetTextSize(maxStrMsg, FONT_SIZE, FONT, Vector2.new())
}
end
end
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, LINE_PADDING),
})
elements["WindowingPadding"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, paddingHeight),
BackgroundTransparency = 1,
LayoutOrder = 1,
})
end
return Roact.createElement("ScrollingFrame", {
Size = size,
BackgroundTransparency = 1,
VerticalScrollBarInset = 1,
ScrollBarThickness = 6,
CanvasSize = UDim2.new(0, 0, 0, scrollingFrameHeight),
CanvasPosition = canvasPos,
LayoutOrder = layoutOrder,
[Roact.Ref] = self.ref,
}, elements)
end
return LogOutput
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Signal = require(script.Parent.Parent.Parent.Signal)
local LogOutput = require(script.Parent.LogOutput)
it("should create and destroy without errors", function()
local element = Roact.createElement(LogOutput, {
targetSignal = Signal.new()
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,191 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ClientLog = require(Components.Log.ClientLog)
local ServerLog = require(Components.Log.ServerLog)
local UtilAndTab = require(Components.UtilAndTab)
local DataConsumer = require(Components.DataConsumer)
local Actions = script.Parent.Parent.Parent.Actions
local SetActiveTab = require(Actions.SetActiveTab)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MsgTypeNamesOrdered = Constants.MsgTypeNamesOrdered
local MainViewLog = Roact.PureComponent:extend("MainViewLog")
function MainViewLog:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onClientButton = function()
self.props.dispatchSetActiveTab("Log", true)
end
self.onServerButton = function()
self.props.dispatchSetActiveTab("Log", false)
end
self.onCheckBoxChanged = function(boxName, newState)
if self.props.isClientView then
self.props.ClientLogData:setFilter(boxName, newState)
else
self.props.ServerLogData:setFilter(boxName, newState)
end
end
self.filterUpdated = function()
self:setState({})
end
self.onSearchTermChanged = function(newSearchTerm)
if self.props.isClientView then
self.props.ClientLogData:setSearchTerm(newSearchTerm)
else
self.props.ServerLogData:setSearchTerm(newSearchTerm)
end
end
local clientFilterSignal = self.props.ClientLogData:filterUpdatedSignal()
self.clientFilterConnection = clientFilterSignal:Connect(self.filterUpdated)
local serverFilterSignal = self.props.ServerLogData:filterUpdatedSignal()
self.serverFilterConnection = serverFilterSignal:Connect(self.filterUpdated)
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0
}
end
function MainViewLog:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewLog:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewLog:willUnmount()
if self.clientFilterConnection then
self.clientFilterConnection:Disconnect()
self.clientFilterConnection = nil
end
if self.serverFilterConnection then
self.serverFilterConnection:Disconnect()
self.serverFilterConnection = nil
end
end
function MainViewLog:render()
local size = self.props.size
local formFactor = self.props.formFactor
local isDeveloperView = self.props.isDeveloperView
local tabList = self.props.tabList
local isClientView = self.props.isClientView
local utilTabHeight = self.state.utilTabHeight
local searchTerm
if isClientView then
searchTerm = self.props.ClientLogData:getSearchTerm()
else
searchTerm = self.props.ServerLogData:getSearchTerm()
end
local elements = {}
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, PADDING),
})
local currCheckBoxState
if isClientView then
currCheckBoxState = self.props.ClientLogData:getFilters()
else
currCheckBoxState = self.props.ServerLogData:getFilters()
end
local initCheckBoxes = {}
for i, v in ipairs(MsgTypeNamesOrdered) do
initCheckBoxes[i] = {
name = v,
state = currCheckBoxState[v],
}
end
elements ["UtilAndTab"] = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
orderedCheckBoxState = initCheckBoxes,
isClientView = isClientView,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onClientButton = isDeveloperView and self.onClientButton,
onServerButton = isDeveloperView and self.onServerButton,
onCheckBoxChanged = self.onCheckBoxChanged,
onSearchTermChanged = self.onSearchTermChanged,
})
if utilTabHeight > 0 then
if isClientView then
elements["ClientLog"] = Roact.createElement(ClientLog, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
layoutOrder = 2,
})
else
elements["ServerLog"] = Roact.createElement(ServerLog, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
layoutOrder = 2,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, elements)
end
local function mapStateToProps(state, props)
return {
isClientView = state.MainView.isClientView
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchSetActiveTab = function (tabListIndex, isClientView)
dispatch(SetActiveTab(tabListIndex, isClientView))
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(
DataConsumer(MainViewLog, "ClientLogData", "ServerLogData")
)
@@ -0,0 +1,41 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewLog = require(script.Parent.MainViewLog)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
LogData = {
clientSearchTerm = "",
clientTypeFilters = {},
serverSearchTerm = "",
serverTypeFilters = {},
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewLog = Roact.createElement(MainViewLog, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local DataConsumer = require(script.Parent.Parent.DataConsumer)
local Constants = require(script.Parent.Parent.Parent.Constants)
local COMMANDLINE_HEIGHT = Constants.LogFormatting.CommandLineHeight
local COMMANDLINE_PADDING = 4
local DevConsoleCommandLine = require(script.Parent.DevConsoleCommandLine)
local LogOutput = require(script.Parent.LogOutput)
local ServerLog = Roact.Component:extend("ServerLog")
function ServerLog:init()
self.initServerLogData = function()
return self.props.ServerLogData:getLogData()
end
end
function ServerLog:render()
return Roact.createElement("Frame", {
Size = self.props.size,
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
}, {
Scroll = Roact.createElement(LogOutput, {
size = UDim2.new(1, 0 , 1, -(COMMANDLINE_HEIGHT + COMMANDLINE_PADDING)),
targetSignal = self.props.ServerLogData:Signal(),
initLogOutput = self.initServerLogData,
}),
CommandLine = Roact.createElement(DevConsoleCommandLine, {
pos = UDim2.new(0,0,1,-COMMANDLINE_HEIGHT),
height = COMMANDLINE_HEIGHT
}),
})
end
return DataConsumer(ServerLog, "ServerLogData")
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local ServerLog = require(script.Parent.ServerLog)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
ServerLog = Roact.createElement(ServerLog)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local MemoryView = require(Components.Memory.MemoryView)
local ClientMemory = Roact.Component:extend("ClientMemory")
function ClientMemory:render()
return Roact.createElement(MemoryView, {
layoutOrder = self.props.layoutOrder,
size = self.props.size,
searchTerm = self.props.searchTerm,
targetMemoryData = self.props.ClientMemoryData,
})
end
return DataConsumer(ClientMemory, "ClientMemoryData")
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local ClientMemory = require(script.Parent.ClientMemory)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
ClientMemory = Roact.createElement(ClientMemory)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,404 @@
local Signal = require(script.Parent.Parent.Parent.Signal)
local SoundService = game:GetService("SoundService")
local MeshContentProvider = game:GetService("MeshContentProvider")
local KeyframeSequenceProvider = game:GetService("KeyframeSequenceProvider")
local StatsService = game:GetService("Stats")
local StatsUtils = require(script.Parent.Parent.Parent.Parent.Stats.StatsUtils)
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames
local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount"))
local CLIENT_POLLING_INTERVAL = 3 -- seconds
local BYTE_IN_MB = 1048576
local SORT_COMPARATOR = {
[HEADER_NAMES[1]] = function(a, b)
return a.name < b.name
end,
[HEADER_NAMES[2]] = function(a, b)
return a.dataStats.dataSet:back().data < b.dataStats.dataSet:back().data
end,
}
local ClientMemoryData = {}
ClientMemoryData.__index = ClientMemoryData
function ClientMemoryData.new()
local self = {}
setmetatable(self, ClientMemoryData)
self._pollingId = 0
self._totalMemory = 0
self._memoryData = {}
self._memoryDataSorted = {}
self._treeViewUpdatedSignal = Signal.new()
self._totalMemoryUpdated = Signal.new()
self._sortType = HEADER_NAMES[1]
self._isRunning = false
self._doGranularMemUpdate = {}
self._granularMemTable = {}
return self
end
local function GetMemoryPerformanceStatsItem()
local performanceStats = StatsService and StatsService:FindFirstChild("PerformanceStats")
if not performanceStats then
return nil
end
local memoryStats = performanceStats:FindFirstChild(
StatsUtils.StatNames[StatsUtils.StatType_Memory])
return memoryStats
end
-- the fetch memoryData functions are used for the Granular memory data.
local function fetchSoundMemoryData()
local soundMemData = SoundService:GetSoundMemoryData()
local sortedSoundMem = {}
for i,v in pairs(soundMemData) do
table.insert(sortedSoundMem, {
name = i,
value = v,
})
end
-- entries are ordered by insert time
-- we want it sorted by memory size
-- we could also have it sorted in SoundService to maybe speed this up
table.sort(sortedSoundMem, function(a, b)
return a.value > b.value
end)
return sortedSoundMem
end
local function fetchGraphicsTextureMemoryData()
local textureData = StatsService:GetPaginatedMemoryByTexture(Enum.TextureQueryType.NonHumanoid, 0, 100)
local textureData2 = StatsService:GetPaginatedMemoryByTexture(Enum.TextureQueryType.NonHumanoidOrphaned, 0, 100)
local sortedTextureData = {}
local function aggregateData(retData, data)
for _,v in ipairs(data.Results) do
local mem = v.MemoryInBytes / BYTE_IN_MB
table.insert(retData, {
name = v.TextureId,
value = mem,
})
end
end
aggregateData(sortedTextureData, textureData)
aggregateData(sortedTextureData, textureData2)
table.sort(sortedTextureData, function(a, b)
return a.value > b.value
end)
return sortedTextureData
end
local function fetchGraphicsTextureCharacterMemoryData()
local textureData = StatsService:GetPaginatedMemoryByTexture(Enum.TextureQueryType.Humanoid, 0, 100)
local textureData2 = StatsService:GetPaginatedMemoryByTexture(Enum.TextureQueryType.HumanoidOrphaned, 0, 100)
local sortedTextureData = {}
local function aggregateData(retData, data)
for _,v in ipairs(data.Results) do
local mem = v.MemoryInBytes / BYTE_IN_MB
local compTextures = {}
-- we need to parse out the asset id's from this large string
-- the first portion involves only including the Texture portions and not color
local txtComp = v.TextureId
local tSeries = {}
for a,b in pairs(string.split(txtComp, " ")) do
local firstChar = string.sub(b, 1, 2)
if firstChar == "T[" then
table.insert(tSeries, b)
end
end
-- next we split out the mesh from the string.
-- we can have multiple references to same ID so
-- so we filter them out here
local assetString = ""
local dedupe = {}
for a, b in pairs(tSeries) do
local gotoAssetStr = string.split(b, ".mesh:")[2]
-- set to 12 to get pass the ":'" in "rbxassertid:" and "http://""
local matchIndex = string.find(gotoAssetStr, ":", 12)
-- we dont want to include the ":" we found
if matchIndex ~= nil then
matchIndex = matchIndex - 1
end
assetString = string.sub(gotoAssetStr, 1, matchIndex)
dedupe[assetString] = true
end
-- using the finalized strings, we construct the list of assets used for the
-- composite texture
for name,_ in pairs(dedupe) do
table.insert(compTextures, {
name = name
})
end
table.sort(compTextures, function(a, b)
return a.name < b.name
end)
table.insert(retData, {
name = "Composite Texture",
value = mem,
moreInfo = compTextures,
})
end
end
aggregateData(sortedTextureData, textureData)
aggregateData(sortedTextureData, textureData2)
return sortedTextureData
end
local function fetchGraphicsMeshPartsMemoryData()
local meshData = MeshContentProvider:getContentMemoryData()
local sortedMeshData = {}
for name, bytes in pairs(meshData) do
local mem = bytes / BYTE_IN_MB
table.insert(sortedMeshData, {
name = name,
value = mem,
})
end
table.sort(sortedMeshData, function(a, b)
return a.value > b.value
end)
return sortedMeshData
end
local function fetchFuncAnimation()
local memstats = KeyframeSequenceProvider:GetMemStats()
local sortedAnimationData = {}
for name, bytes in pairs(memstats) do
local mem = bytes / BYTE_IN_MB
table.insert(sortedAnimationData, {
name = name,
value = mem,
})
end
table.sort(sortedAnimationData, function(a, b)
return a.value > b.value
end)
return sortedAnimationData
end
function ClientMemoryData:updateCachedData(categoryName, retrieveDataCallback)
if self._doGranularMemUpdate[categoryName] then
self._doGranularMemUpdate[categoryName] = false
self._granularMemTable[categoryName] = retrieveDataCallback()
end
end
function ClientMemoryData:getAdditionalMemoryFunc(name)
local fetchFunc = nil
if name == "Sounds" then
fetchFunc = fetchSoundMemoryData
elseif name == "GraphicsTexture" then
fetchFunc = fetchGraphicsTextureMemoryData
elseif name == "GraphicsTextureCharacter" then
fetchFunc = fetchGraphicsTextureCharacterMemoryData
elseif name == "GraphicsMeshParts" then
fetchFunc = fetchGraphicsMeshPartsMemoryData
elseif name == "csgDictionary" then
-- this case requires more work to properly reflect the desired changes
elseif name == "Animation" then
fetchFunc = fetchFuncAnimation
end
if fetchFunc then
return function()
self:updateCachedData(name, fetchFunc)
return self._granularMemTable[name]
end
end
return nil
end
function ClientMemoryData:recursiveUpdateEntry(entryList, sortedList, statsItem)
local name = StatsUtils.GetMemoryAnalyzerStatName(statsItem.Name)
local data = statsItem:GetValue()
local children = statsItem:GetChildren()
if not entryList[name] then
local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT)
newBuffer:push_back({
data = data,
time = self._lastUpdate
})
entryList[name] = {
min = data,
max = data,
dataSet = newBuffer,
children = #children > 0 and {},
sortedChildren = #children > 0 and {},
}
-- Mem Data is aggregated from allocations and deallocations and
-- does not know about where the allocations come from. So, that data
-- can't be par't of the tree we get from Stats
-- We attach a callback for the specific entries that need to
-- show addition memory to handle this.
local memFunc = self:getAdditionalMemoryFunc(name)
if memFunc then
entryList[name]["additionalInfoFunc"] = memFunc
self._doGranularMemUpdate[name] = true
end
local newEntry = {
name = name,
dataStats = entryList[name]
}
table.insert(sortedList, newEntry)
else
local currMax = entryList[name].max
local currMin = entryList[name].min
local update = {
data = data,
time = self._lastUpdate
}
-- if there is any change from the previous entry, flag
-- this category for additionaInfo Update
if entryList[name]["additionalInfoFunc"] then
local last = entryList[name].dataSet:back();
if last.data ~= update.data then
self._doGranularMemUpdate[name] = true
end
end
local overwrittenEntry = entryList[name].dataSet:push_back(update)
if overwrittenEntry then
local iter = entryList[name].dataSet:iterator()
local dat = iter:next()
if currMax == overwrittenEntry.data then
currMax = currMin
while dat do
currMax = dat.data < currMax and currMax or dat.data
dat = iter:next()
end
end
if currMin == overwrittenEntry.data then
currMin = currMax
while dat do
currMin = currMin < dat.data and currMin or dat.data
dat = iter:next()
end
end
end
entryList[name].max = currMax < data and data or currMax
entryList[name].min = currMin < data and currMin or data
end
for _, childStatItem in ipairs(children) do
self:recursiveUpdateEntry(
entryList[name].children,
entryList[name].sortedChildren,
childStatItem
)
end
end
function ClientMemoryData:totalMemSignal()
return self._totalMemoryUpdated
end
function ClientMemoryData:treeUpdatedSignal()
return self._treeViewUpdatedSignal
end
function ClientMemoryData:getSortType()
return self._sortType
end
local function recursiveSort(memoryDataSort, comparator)
table.sort(memoryDataSort, comparator)
for _, entry in pairs(memoryDataSort) do
if entry.dataStats.sortedChildren then
recursiveSort(entry.dataStats.sortedChildren, comparator)
end
end
end
function ClientMemoryData:setSortType(sortType)
if SORT_COMPARATOR[sortType] then
self._sortType = sortType
-- do we need a mutex type thing here?
recursiveSort(self._memoryDataSorted, SORT_COMPARATOR[self._sortType])
else
error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2)
end
end
function ClientMemoryData:getMemoryData()
return self._memoryDataSorted
end
function ClientMemoryData:isRunning()
return self._isRunning
end
function ClientMemoryData:start()
spawn(function()
self._pollingId = self._pollingId + 1
local instanced_pollingId = self._pollingId
self._isRunning = true
while instanced_pollingId == self._pollingId do
local statsItem = GetMemoryPerformanceStatsItem()
if not statsItem then
return
end
self._lastUpdate = os.time()
self:recursiveUpdateEntry(self._memoryData, self._memoryDataSorted, statsItem)
if self._totalMemory ~= statsItem:getValue() then
self._totalMemory = statsItem:getValue()
self._totalMemoryUpdated:Fire(self._totalMemory)
end
self._treeViewUpdatedSignal:Fire(self._memoryDataSorted)
wait(CLIENT_POLLING_INTERVAL)
end
self._isRunning = false
end)
end
function ClientMemoryData:stop()
-- listeners are responsible for disconnecting themselves
self._pollingId = self._pollingId + 1
end
return ClientMemoryData
@@ -0,0 +1,191 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ClientMemory = require(Components.Memory.ClientMemory)
local ServerMemory = require(Components.Memory.ServerMemory)
local UtilAndTab = require(Components.UtilAndTab)
local DataConsumer = require(Components.DataConsumer)
local Actions = script.Parent.Parent.Parent.Actions
local ClientMemoryUpdateSearchFilter = require(Actions.ClientMemoryUpdateSearchFilter)
local ServerMemoryUpdateSearchFilter = require(Actions.ServerMemoryUpdateSearchFilter)
local Constants = require(script.Parent.Parent.Parent.Constants)
local MAIN_ROW_PADDING = Constants.GeneralFormatting.MainRowPadding
-- may want to move LogButton into its own module
local LogButton = Roact.Component:extend("LogButton")
function LogButton:init()
self.onAction = function()
local data = self.props.isClientView and self.props.ClientMemoryData or self.props.ServerMemoryData
local result = "\n"
local function line(str) result = result .. str .. "\n" end
line("Name Min Max")
line("---------------------------------------- --- ---")
local function recurseMemoryTree(name, what, indent)
line(string.format("%s %s %s %3d %3d",
string.rep(" ", indent),
name,
string.rep(" ", 40 - indent - #name),
what.min, what.max
))
if what.children then
for subname,tree in pairs(what.children) do
recurseMemoryTree(subname, tree, indent+2)
end
end
end
recurseMemoryTree("Memory", data._memoryData.Memory, 0)
print(result)
end
end
function LogButton:render()
return Roact.createElement("TextButton", {
Text = "Log",
Visible = true,
BorderSizePixel = 1,
BackgroundColor3 = Constants.Color.UnselectedGray,
TextColor3 = Constants.Color.Text,
[Roact.Event.Activated] = self.onAction
})
end
LogButton = DataConsumer(LogButton, "ClientMemoryData", "ServerMemoryData")
local MainViewMemory = Roact.Component:extend("MainViewMemory")
function MainViewMemory:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onClientButton = function()
self:setState({isClientView = true})
end
self.onServerButton = function()
self:setState({isClientView = false})
end
self.onSearchTermChanged = function(newSearchTerm)
if self.state.isClientView then
self.props.dispatchClientMemoryUpdateSearchFilter(newSearchTerm, {})
else
self.props.dispatchServerMemoryUpdateSearchFilter(newSearchTerm, {})
end
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0,
isClientView = true,
}
end
function MainViewMemory:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset,
})
end
function MainViewMemory:didUpdate()
local utilSize = self.utilRef.current.Size
local height = utilSize.Y.Offset
if height ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = height,
})
end
end
function MainViewMemory:render()
local elements = {}
local size = self.props.size
local isDeveloperView = self.props.isDeveloperView
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local utilTabHeight = self.state.utilTabHeight
local isClientView = self.state.isClientView
local searchTerm = isClientView and self.props.clientSearchTerm or self.props.serverSearchTerm
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, MAIN_ROW_PADDING),
})
elements ["UtilAndTab"] = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
isClientView = isClientView,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onClientButton = isDeveloperView and self.onClientButton,
onServerButton = isDeveloperView and self.onServerButton,
onSearchTermChanged = self.onSearchTermChanged,
}, {
LogButton and Roact.createElement(LogButton, {isClientView = isClientView}),
})
if utilTabHeight > 0 then
if isClientView then
elements["ClientMemory"] = Roact.createElement(ClientMemory, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
else
elements["ServerMemory"] = Roact.createElement(ServerMemory, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, elements)
end
local function mapStateToProps(state, props)
return {
clientSearchTerm = state.MemoryData.clientSearchTerm,
clientTypeFilters = state.MemoryData.clientTypeFilters,
serverSearchTerm = state.MemoryData.serverSearchTerm,
serverTypeFilters = state.MemoryData.serverTypeFilters,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchClientMemoryUpdateSearchFilter = function(searchTerm, filters)
dispatch(ClientMemoryUpdateSearchFilter(searchTerm, filters))
end,
dispatchServerMemoryUpdateSearchFilter = function(searchTerm, filters)
dispatch(ServerMemoryUpdateSearchFilter(searchTerm, filters))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewMemory)
@@ -0,0 +1,40 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewMemory = require(script.Parent.MainViewMemory)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
MemoryData = {
clientSearchTerm = "",
clientTypeFilters = {},
serverSearchTerm = "",
serverTypeFilters = {},
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewMemory = Roact.createElement(MainViewMemory, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,361 @@
local CorePackages = game:GetService("CorePackages")
local TweenService = game:GetService("TweenService")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local HeaderButton = require(Components.HeaderButton)
local MemoryViewEntry = require(script.Parent.MemoryViewEntry)
local Constants = require(script.Parent.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames
local HEADER_HEIGHT = Constants.GeneralFormatting.HeaderFrameHeight
local VALUE_CELL_WIDTH = Constants.MemoryFormatting.ValueCellWidth
local CELL_PADDING = Constants.MemoryFormatting.CellPadding
local VALUE_PADDING = Constants.MemoryFormatting.ValuePadding
local ENTRY_HEIGHT = Constants.GeneralFormatting.EntryFrameHeight
local GRAPH_HEIGHT = Constants.GeneralFormatting.LineGraphHeight
local NO_RESULT_SEARCH_STR = Constants.GeneralFormatting.NoResultSearchStr
local tweenInfo = TweenInfo.new(
.3, -- Time
Enum.EasingStyle.Back,
Enum.EasingDirection.Out,
0,
false,
0
)
local MemoryView = Roact.Component:extend("MemoryView")
local function getX(entry)
return entry.time
end
local function getY(entry)
return entry.data
end
local function formatValueStr(value)
return string.format("%.3f", value)
end
function MemoryView:init(props)
self.getOnButtonPress = function (name, index)
return function(rbx, input)
if self.state.expandIndex ~= name and name then
local scrollingRef = self.scrollingRef.current
-- LayoutOrder is used to keep track of the order of the entires.
-- We subtrack 1 to account for the position of the top left corner
-- of the entry, and another 1.4 to provide a 1.4 * ENTRY_HEIGHT's
-- worth of buffer space
if scrollingRef then
local newCanvasPosY = (index-2.4) * ENTRY_HEIGHT
TweenService:Create(
scrollingRef,
tweenInfo,
{CanvasPosition = Vector2.new(0, newCanvasPosY)}
):Play()
end
end
self:setState({
expandIndex = self.state.expandIndex ~= name and name
})
end
end
self.onSortChanged = function(sortType)
local currSortType = props.targetMemoryData:getSortType()
if sortType == currSortType then
self:setState({
reverseSort = not self.state.reverseSort
})
else
props.targetMemoryData:setSortType(sortType)
self:setState({
reverseSort = false,
})
end
end
self.onCanvasPosChanged = function()
local canvasPos = self.scrollingRef.current.CanvasPosition
if self.state.canvasPos ~= canvasPos then
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = canvasPos,
})
end
end
self.scrollingRef = Roact.createRef()
self.state = {
memoryData = props.targetMemoryData:getMemoryData(),
reverseSort = false,
expandIndex = false,
}
end
function MemoryView:willUpdate()
if self.canvasPosConnector then
self.canvasPosConnector:Disconnect()
end
end
function MemoryView:didUpdate()
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
end
end
function MemoryView:didMount()
local treeUpdatedSignal = self.props.targetMemoryData:treeUpdatedSignal()
self.treeViewItemConnector = treeUpdatedSignal:Connect(function(memoryData)
self:setState({
memoryData = memoryData
})
end)
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = self.scrollingRef.current.CanvasPosition,
})
end
end
function MemoryView:willUnmount()
self.treeViewItemConnector:Disconnect()
end
function MemoryView:appendAdditionTabInformation(elements, infoTable, parentName, depth, windowing)
local canvasPos = self.scrollingRef.current.CanvasPosition
local absScrollSize = self.scrollingRef.current.AbsoluteSize
-- this function, where applicable is in clientMemoryData
for _,additionalEntry in ipairs(infoTable) do
local name = additionalEntry.name
local value = additionalEntry.value
local new_key = parentName .. name
windowing.layoutOrder = windowing.layoutOrder + 1
if windowing.scrollingFrameHeight + ENTRY_HEIGHT >= canvasPos.Y then
if windowing.usedFrameSpace < absScrollSize.Y then
local new_key = parentName .. name
elements[new_key] = Roact.createElement(MemoryViewEntry, {
size = UDim2.new(1, 0, 0, ENTRY_HEIGHT),
depth = depth,
name = name,
showGraph = false,
value = value,
formatValueStr = formatValueStr,
layoutOrder = windowing.layoutOrder,
})
if windowing.paddingHeight < 0 then
windowing.paddingHeight = windowing.scrollingFrameHeight
else
windowing.usedFrameSpace = windowing.usedFrameSpace + ENTRY_HEIGHT
end
end
end
windowing.scrollingFrameHeight = windowing.scrollingFrameHeight + ENTRY_HEIGHT
if additionalEntry.moreInfo and type(additionalEntry.moreInfo) == "table" then
self:appendAdditionTabInformation(elements, additionalEntry.moreInfo, new_key, depth + 1, windowing)
end
end
end
function MemoryView:recursiveConstructEntries(elements, entry, depth, windowing)
assert(self.scrollingRef.current, "ScrollingFrame not initialized yet")
local expandIndex = self.state.expandIndex
local searchTerm = self.props.searchTerm or ""
local reverseSort = self.state.reverseSort
local canvasPos = self.scrollingRef.current.CanvasPosition
local absScrollSize = self.scrollingRef.current.AbsoluteSize
local name = entry.name
local found = string.find(name:lower(), searchTerm:lower())
if found then
local showGraph = expandIndex == name
local frameHeight = showGraph and ENTRY_HEIGHT + GRAPH_HEIGHT or ENTRY_HEIGHT
windowing.layoutOrder = windowing.layoutOrder + 1
if windowing.scrollingFrameHeight + frameHeight >= canvasPos.Y then
if windowing.usedFrameSpace < absScrollSize.Y then
elements[name] = Roact.createElement(MemoryViewEntry, {
size = UDim2.new(1, 0, 0, frameHeight),
depth = depth,
name = entry.name,
showGraph = showGraph,
dataStats = entry.dataStats,
onButtonPress = self.getOnButtonPress(name, windowing.layoutOrder),
formatValueStr = formatValueStr,
getX = getX,
getY = getY,
layoutOrder = windowing.layoutOrder,
})
if windowing.paddingHeight < 0 then
windowing.paddingHeight = windowing.scrollingFrameHeight
else
windowing.usedFrameSpace = windowing.usedFrameSpace + frameHeight
end
end
end
windowing.scrollingFrameHeight = windowing.scrollingFrameHeight + frameHeight
-- callback is set in ClientMemoryData
if showGraph then
-- this function, where applicable is in clientMemoryData
if entry.dataStats.additionalInfoFunc then
local infoTable = entry.dataStats.additionalInfoFunc()
self:appendAdditionTabInformation(elements, infoTable, entry.name, depth + 1, windowing)
end
end
end
local sortedChildren = entry.dataStats.sortedChildren
if sortedChildren then
if reverseSort then
local totalChildren = #sortedChildren
for i = 1, totalChildren do
self:recursiveConstructEntries(elements, sortedChildren[totalChildren - i + 1], depth + 1, windowing)
end
else
for _, entry in ipairs(sortedChildren) do
self:recursiveConstructEntries(elements, entry, depth + 1, windowing)
end
end
end
end
function MemoryView:render()
local elements = {}
local layoutOrder = self.props.layoutOrder
local size = self.props.size
local searchTerm = self.props.searchTerm or ""
-- we pass this table into the recursion to keep sum up
-- height totals for windowing
local windowingInfo = {
scrollingFrameHeight = 0,
paddingHeight = -1,
usedFrameSpace = 0,
layoutOrder = 1
}
if self.scrollingRef.current then
for _, entry in ipairs(self.state.memoryData) do
self:recursiveConstructEntries(elements, entry, 0, windowingInfo)
end
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
elements["WindowingPadding"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, windowingInfo.paddingHeight),
BackgroundTransparency = 1,
LayoutOrder = 1,
})
end
if windowingInfo.layoutOrder == 1 then
if searchTerm == "" then
elements["noDataMessage"] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
Text = "Awaiting Memory Stats",
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
else
local noResultSearchStr = string.format(NO_RESULT_SEARCH_STR, searchTerm )
elements["noDataMessage"] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
Text = noResultSearchStr,
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
Header = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
}, {
Name = Roact.createElement(HeaderButton, {
text = HEADER_NAMES[1],
size = UDim2.new(1 - VALUE_CELL_WIDTH, -VALUE_PADDING - CELL_PADDING, 0, HEADER_HEIGHT),
pos = UDim2.new(0, CELL_PADDING, 0, 0),
sortfunction = self.onSortChanged,
}),
ValueMB = Roact.createElement(HeaderButton, {
text = HEADER_NAMES[2],
size = UDim2.new( VALUE_CELL_WIDTH, -CELL_PADDING, 0, HEADER_HEIGHT),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, VALUE_PADDING, 0, 0),
sortfunction = self.onSortChanged,
}),
TopHorizontal = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
LowerHorizontal = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
vertical = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
Entries = Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
Position = UDim2.new(0, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
VerticalScrollBarInset = 1,
ScrollBarThickness = 5,
CanvasSize = UDim2.new(1, 0, 0, windowingInfo.scrollingFrameHeight),
[Roact.Ref] = self.scrollingRef,
}, elements),
})
end
return MemoryView
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Signal = require(script.Parent.Parent.Parent.Signal)
local MemoryView = require(script.Parent.MemoryView)
local dummmyMemoryData = {
getMemoryData = function ()
return {
summaryTable = {},
summaryCount = 0,
entryList = nil,
}
end,
treeUpdatedSignal = function ()
return Signal.new()
end,
}
it("should create and destroy without errors", function()
local element = Roact.createElement(MemoryView, {
targetMemoryData = dummmyMemoryData,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,104 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local CellLabel = require(Components.CellLabel)
local BannerButton = require(Components.BannerButton)
local LineGraph = require(Components.LineGraph)
local Constants = require(script.Parent.Parent.Parent.Constants)
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local VALUE_CELL_WIDTH = Constants.MemoryFormatting.ValueCellWidth
local CELL_PADDING = Constants.MemoryFormatting.CellPadding
local VALUE_PADDING = Constants.MemoryFormatting.ValuePadding
local ENTRY_HEIGHT = Constants.GeneralFormatting.EntryFrameHeight
local DEPTH_INDENT = Constants.MemoryFormatting.DepthIndent
local convertTimeStamp = require(script.Parent.Parent.Parent.Util.convertTimeStamp)
return function(props)
local size = props.size
local depth = props.depth
local showGraph = props.showGraph
local dataStats = props.dataStats
local layoutOrder = props.layoutOrder
local onButtonPress = props.onButtonPress
local formatValueStr = props.formatValueStr
local getX = props.getX
local getY = props.getY
local offset = depth * DEPTH_INDENT
local name = props.name
local value = props.value
if dataStats then
value = dataStats.dataSet:back().data
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
button = Roact.createElement(BannerButton, {
size = UDim2.new(1, - offset, 0, ENTRY_HEIGHT),
pos = UDim2.new(0, offset, 0, 0),
isExpanded = showGraph,
onButtonPress = onButtonPress,
}, {
name = Roact.createElement(CellLabel, {
text = name,
size = UDim2.new(1, 0, 1, 0),
pos = UDim2.new(0, CELL_PADDING, 0, 0),
}),
horizonal1 = Roact.createElement("Frame", {
Size = UDim2.new(1, -offset , 0, LINE_WIDTH),
Position = UDim2.new(0, offset, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
horizonal2 = Roact.createElement("Frame", {
Size = UDim2.new(1, -offset, 0, LINE_WIDTH),
Position = UDim2.new(0, offset, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
}),
value = value and Roact.createElement(CellLabel, {
text = formatValueStr(value),
size = UDim2.new(VALUE_CELL_WIDTH, -VALUE_PADDING, 0, ENTRY_HEIGHT),
pos = UDim2.new(1 - VALUE_CELL_WIDTH, VALUE_PADDING, 0, 0),
}),
vertical = Roact.createElement("Frame", {
Size = UDim2.new(0, 1, 0, ENTRY_HEIGHT),
Position = UDim2.new(1 - VALUE_CELL_WIDTH, 0, 0, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
}),
Graph = showGraph and Roact.createElement(LineGraph, {
pos = UDim2.new(0, 0, 0, ENTRY_HEIGHT),
size = UDim2.new(1, 0, 1, -ENTRY_HEIGHT),
graphData = dataStats.dataSet,
maxY = dataStats.max,
minY = dataStats.min,
getX = getX,
getY = getY,
stringFormatX = convertTimeStamp,
stringFormatY = formatValueStr,
axisLabelX = "Timestamp",
axisLabelY = name,
}),
})
end
@@ -0,0 +1,56 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local DataProvider = require(script.Parent.Parent.DataProvider)
local MemoryViewEntry = require(script.Parent.MemoryViewEntry)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local dummyScriptEntry = {
time = 0,
data = {0, 0},
}
local dummyDataSet = CircularBuffer.new(1)
dummyDataSet:push_back(dummyScriptEntry)
local formatValueStr = function()
return ""
end
local dummyEntry = {
name = "",
dataStats = {
dataSet = dummyDataSet,
}
}
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MemoryViewEntry = Roact.createElement(MemoryViewEntry, {
depth = 0,
entry = dummyEntry,
formatValueStr = formatValueStr,
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local MemoryView = require(Components.Memory.MemoryView)
local ServerMemory = Roact.Component:extend("ServerMemory")
function ServerMemory:render()
return Roact.createElement(MemoryView, {
layoutOrder = self.props.layoutOrder,
size = self.props.size,
searchTerm = self.props.searchTerm,
targetMemoryData = self.props.ServerMemoryData,
})
end
return DataConsumer(ServerMemory, "ServerMemoryData")
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local ServerMemory = require(script.Parent.ServerMemory)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, nil, {
ServerMemory = Roact.createElement(ServerMemory)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,239 @@
local Signal = require(script.Parent.Parent.Parent.Signal)
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.MemoryFormatting.ChartHeaderNames
local MAX_DATASET_COUNT = tonumber(settings():GetFVariable("NewDevConsoleMaxGraphCount"))
local BYTES_PER_MB = 1048576.0
local SORT_COMPARATOR = {
[HEADER_NAMES[1]] = function(a, b)
return a.name < b.name
end,
[HEADER_NAMES[2]] = function(a, b)
return a.dataStats.dataSet:back().data < b.dataStats.dataSet:back().data
end,
}
local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator)
local ServerMemoryData = {}
ServerMemoryData.__index = ServerMemoryData
function ServerMemoryData.new()
local self = {}
setmetatable(self, ServerMemoryData)
self._init = false
self._isRunning = false
self._totalMemory = 0
self._memoryData = {}
self._memoryDataSorted = {}
self._coreTreeData = {}
self._coreTreeDataSorted = {}
self._placeTreeData = {}
self._placeTreeDataSorted = {}
self._treeViewUpdated = Signal.new()
self._sortType = HEADER_NAMES[1]
return self
end
function ServerMemoryData:updateEntry(entryList, sortedList, name, data)
if not entryList[name] then
local newBuffer = CircularBuffer.new(MAX_DATASET_COUNT)
newBuffer:push_back({
data = data,
time = self._lastUpdate
})
entryList[name] = {
min = data,
max = data,
dataSet = newBuffer
}
local newEntry = {
name = name,
dataStats = entryList[name]
}
table.insert(sortedList, newEntry)
else
local currMax = entryList[name].max
local currMin = entryList[name].min
local update = {
data = data,
time = self._lastUpdate
}
local overwrittenEntry = entryList[name].dataSet:push_back(update)
if overwrittenEntry then
local iter = entryList[name].dataSet:iterator()
local dat = iter:next()
if currMax == overwrittenEntry.data then
currMax = currMin
while dat do
currMax = dat.data < currMax and currMax or dat.data
dat = iter:next()
end
end
if currMin == overwrittenEntry.data then
currMin = currMax
while dat do
currMin = currMin < dat.data and currMin or dat.data
dat = iter:next()
end
end
end
entryList[name].max = currMax < data and data or currMax
entryList[name].min = currMin < data and currMin or data
end
end
function ServerMemoryData:updateEntryList(entryList, sortedList, statsItems)
-- All values are in bytes.
-- Convert to MB ASAP.
local totalMB = 0
for label, numBytes in pairs(statsItems) do
local value = numBytes / BYTES_PER_MB
totalMB = totalMB + value
self:updateEntry(entryList, sortedList, label, value)
end
return totalMB
end
function ServerMemoryData:updateWithTreeStats(stats)
local update = {
PlaceMemory = 0,
CoreMemory = 0,
UntrackedMemory = 0,
}
for key, value in pairs(stats) do
if key == "totalServerMemory" then
self._totalMemory = value / BYTES_PER_MB
elseif key == "developerTags" then
update.PlaceMemory = self:updateEntryList(self._placeTreeData, self._placeTreeDataSorted, value)
elseif key == "internalCategories" then
update.CoreMemory = self:updateEntryList(self._coreTreeData, self._coreTreeDataSorted, value)
end
end
update.UntrackedMemory = self._totalMemory - update.PlaceMemory - update.CoreMemory
if self._init then
for name, value in pairs(update) do
self:updateEntry(
self._memoryData["Memory"].children,
self._memoryData["Memory"].sortedChildren,
name,
value
)
end
self:updateEntry(self._memoryData, self._memoryDataSorted, "Memory", self._totalMemory)
else
local memChildren = {}
local memChildrenSorted = {}
for name, value in pairs(update) do
self:updateEntry(memChildren, memChildrenSorted, name, value)
end
self:updateEntry(self._memoryData, self._memoryDataSorted, "Memory", self._totalMemory)
memChildren["PlaceMemory"].children = self._placeTreeData
memChildren["PlaceMemory"].sortedChildren = self._placeTreeDataSorted
memChildren["CoreMemory"].children = self._coreTreeData
memChildren["CoreMemory"].sortedChildren = self._coreTreeDataSorted
self._memoryData["Memory"].children = memChildren
self._memoryData["Memory"].sortedChildren = memChildrenSorted
self._init = true
end
end
function ServerMemoryData:totalMemSignal()
return self._totalMemoryUpdated
end
function ServerMemoryData:treeUpdatedSignal()
return self._treeViewUpdated
end
function ServerMemoryData:getSortType()
return self._sortType
end
local function recursiveSort(memoryDataSort, comparator)
table.sort(memoryDataSort, comparator)
for _, entry in pairs(memoryDataSort) do
if entry.dataStats.sortedChildren then
recursiveSort(entry.dataStats.sortedChildren, comparator)
end
end
end
function ServerMemoryData:setSortType(sortType)
if SORT_COMPARATOR[sortType] then
self._sortType = sortType
recursiveSort(self._memoryDataSorted, SORT_COMPARATOR[self._sortType])
else
error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2)
end
end
function ServerMemoryData:getMemoryData()
return self._memoryDataSorted
end
function ServerMemoryData:isRunning()
return self._isRunning
end
function ServerMemoryData:start()
local clientReplicator = getClientReplicator()
if clientReplicator and not self._statsListenerConnection then
self._statsListenerConnection = clientReplicator.StatsReceived:connect(function(stats)
if not stats.ServerMemoryTree then
return
end
self._lastUpdate = os.time()
local serverMemoryTree = stats.ServerMemoryTree
if serverMemoryTree then
self:updateWithTreeStats(serverMemoryTree)
self._treeViewUpdated:Fire(self._memoryDataSorted)
end
end)
clientReplicator:RequestServerStats(true)
self._isRunning = true
end
end
function ServerMemoryData:stop()
-- listeners are responsible for disconnecting themselves
local clientReplicator = getClientReplicator()
if clientReplicator then
clientReplicator:RequestServerStats(false)
self._statsListenerConnection:Disconnect()
self._isRunning = false
end
end
return ServerMemoryData
@@ -0,0 +1,213 @@
local CorePackages = game:GetService("CorePackages")
local LogService = game:GetService("LogService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local UtilAndTab = require(Components.UtilAndTab)
local ServerProfilerInterface = require(script.Parent.ServerProfilerInterface)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local BUTTON_WIDTH = Constants.MicroProfilerFormatting.ButtonWidth
local TEXT_SIZE = Constants.MicroProfilerFormatting.ButtonTextSize
local FONT = Constants.Font.MainWindow
local HEADER_FONT = Constants.Font.MainWindowHeader
local BUTTON_UNSELECTED = Constants.Color.UnselectedGray
local BUTTON_SELECTED = Constants.Color.SelectedBlue
local BUTTON_COLOR = Constants.Color.UnselectedGray
local ROW_HEIGHT = 30
local OFFSET = .10
local ROW_VALUE_WIDTH = .8
local MICROPROFILER_PRESSED_COUNTERNAME ="MicroprofilerDevConsolePressed"
local FFlagMicroProfilerSessionAnalytics = settings():GetFFlag("MicroProfilerSessionAnalytics")
local MainViewProfiler = Roact.Component:extend("MainViewProfiler")
function MainViewProfiler:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.changeProfilerState = function(screenProfilerEnabled)
return function()
GameSettings.OnScreenProfilerEnabled = screenProfilerEnabled
self:setState({
clientProfilerEnabled = screenProfilerEnabled
})
if FFlagMicroProfilerSessionAnalytics then
AnalyticsService:ReportCounter(MICROPROFILER_PRESSED_COUNTERNAME)
end
end
end
local microProfilerChangedSignal = GameSettings:GetPropertyChangedSignal("OnScreenProfilerEnabled")
self.microProfilerChangedConnection = microProfilerChangedSignal:Connect(function()
self:setState({
clientProfilerEnabled = GameSettings.OnScreenProfilerEnabled
})
end)
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0,
}
end
function MainViewProfiler:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewProfiler:willUnmount()
if self.microProfilerChangedConnection then
self.microProfilerChangedConnection:Disconnect()
self.microProfilerChangedConnection = nil
end
end
function MainViewProfiler:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewProfiler:render()
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local utilTabHeight = self.state.utilTabHeight
local frameRate = self.state.frameRate
local timeFrame = self.state.timeFrame
local waitingForData = self.state.waitingForData
local clientProfilerEnabled = self.state.clientProfilerEnabled
local outputPath = self.state.outputPath
local textbox_size = UDim2.new(ROW_VALUE_WIDTH, -BUTTON_WIDTH, 0, ROW_HEIGHT)
local textbox_pos = UDim2.new(1 - ROW_VALUE_WIDTH / 2, BUTTON_WIDTH, 0, 0)
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, PADDING),
SortOrder = Enum.SortOrder.LayoutOrder,
}),
UtilAndTab = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
}),
MainFrame = Roact.createElement("ScrollingFrame",{
Size = UDim2.new(1, 0, 1, -utilTabHeight),
CanvasSize = UDim2.new(1, 0, 1, -utilTabHeight),
BackgroundTransparency = 1,
LayoutOrder = 2,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, PADDING * 2),
SortOrder = Enum.SortOrder.LayoutOrder,
}),
MicroProfilerRow = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, ROW_HEIGHT * 3),
BackgroundTransparency = 1,
LayoutOrder = 2,
}, {
Label = Roact.createElement("TextLabel", {
Size = UDim2.new(0, BUTTON_WIDTH, 0, ROW_HEIGHT),
Position = UDim2.new(OFFSET, 0, 0, 0),
Text = "MicroProfiler",
Font = HEADER_FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = BUTTON_COLOR,
BackgroundTransparency = 1,
}),
HorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 0, ROW_HEIGHT),
}),
ToggleButton = Roact.createElement("TextLabel", {
Size = UDim2.new(ROW_VALUE_WIDTH, -BUTTON_WIDTH, 0, ROW_HEIGHT),
Position = UDim2.new((1 - ROW_VALUE_WIDTH)/2, BUTTON_WIDTH, 0, ROW_HEIGHT * 1.25),
BackgroundTransparency = 1,
}, {
OffButton = Roact.createElement("TextButton", {
Size = UDim2.new(.5, 0, 1, 0),
Text = "Off",
Font = HEADER_FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = clientProfilerEnabled and BUTTON_UNSELECTED or BUTTON_SELECTED,
[Roact.Event.Activated] = self.changeProfilerState(false),
}),
ClientButton = Roact.createElement("TextButton", {
Size = UDim2.new(.5, 0, 1, 0),
Position = UDim2.new(.5, 0, 0, 0),
Text = "Client",
Font = HEADER_FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = clientProfilerEnabled and BUTTON_SELECTED or BUTTON_UNSELECTED,
[Roact.Event.Activated] = self.changeProfilerState(true),
}),
}),
}),
HorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1,0,0,PADDING),
BackgroundTransparency = 1,
LayoutOrder = 3
}),
ServerProfiler = Roact.createElement(ServerProfilerInterface, {
Size = UDim2.new(1, 0, 0, ROW_HEIGHT * 4.5),
BackgroundTransparency = 1,
LayoutOrder = 4,
}),
})
})
end
return MainViewProfiler
@@ -0,0 +1,34 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local MainViewMicroProfiler = require(script.Parent.MainViewMicroProfiler)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0
},
MicroProfiler = {
lastFileOutputLocation = ""
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
MainViewMicroProfiler = Roact.createElement(MainViewMicroProfiler, {
size = UDim2.new(),
tabList = {},
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,54 @@
local CorePackages = game:GetService("CorePackages")
local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator)
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Actions = script.Parent.Parent.Parent.Actions
local SetRCCProfilerState = require(Actions.SetRCCProfilerState)
local RCCProfilerDataCompleteListener = Roact.Component:extend("RCCProfilerDataCompleteListener")
function RCCProfilerDataCompleteListener:didMount()
local clientReplicator = getClientReplicator()
if clientReplicator then
self.completeSignal = clientReplicator.RCCProfilerDataComplete:connect(function(success, message)
if self.props.waitingForRecording then
if not success then
warn(message)
self.props.dispatchSetRCCProfilerState(false)
else
self.props.dispatchSetRCCProfilerState(false, message)
end
end
end)
end
end
function RCCProfilerDataCompleteListener:willUnmount()
if self.completeSignal then
self.completeSignal:Disconnect()
self.completeSignal = nil
end
end
function RCCProfilerDataCompleteListener:render()
return nil
end
local function mapStateToProps(state, props)
return {
waitingForRecording = state.MicroProfiler.waitingForRecording,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchSetRCCProfilerState = function(waitingForRecording, fileLocation)
dispatch(SetRCCProfilerState(waitingForRecording, fileLocation))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(RCCProfilerDataCompleteListener)
@@ -0,0 +1,228 @@
local CorePackages = game:GetService("CorePackages")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local getClientReplicator = require(script.Parent.Parent.Parent.Util.getClientReplicator)
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local Actions = script.Parent.Parent.Parent.Actions
local SetRCCProfilerState = require(Actions.SetRCCProfilerState)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local BUTTON_WIDTH = Constants.MicroProfilerFormatting.ButtonWidth
local TEXT_SIZE = Constants.MicroProfilerFormatting.ButtonTextSize
local FONT = Constants.Font.MainWindow
local HEADER_FONT = Constants.Font.MainWindowHeader
local BUTTON_UNSELECTED = Constants.Color.UnselectedGray
local BUTTON_SELECTED = Constants.Color.SelectedBlue
local BUTTON_COLOR = Constants.Color.UnselectedGray
local ROW_HEIGHT = 30
local OFFSET = .10
local ROW_VALUE_WIDTH = .8
local ServerProfilerInteraface = Roact.Component:extend("ServerProfilerInteraface")
function ServerProfilerInteraface:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onFocusLostFrameRate = function(rbx, enterPressed, inputThatCausedFocusLoss)
GameSettings.RCCProfilerRecordFrameRate = rbx.Text
-- the gamesetting value will handle value limits and valid text,
-- therefore we need to reset the textbox Text the correct value after
-- GameSettings has updated it.
rbx.Text = GameSettings.RCCProfilerRecordFrameRate
end
self.onFocusLostTimeFrame = function(rbx, enterPressed, inputThatCausedFocusLoss)
GameSettings.RCCProfilerRecordTimeFrame = rbx.Text
-- the gamesetting value will handle value limits and valid text,
-- therefore we need to reset the textbox Text the correct value after
-- GameSettings has updated it.
rbx.Text = GameSettings.RCCProfilerRecordTimeFrame
end
self.requestRCCProfilerData = function(rbx)
local clientReplicator = getClientReplicator()
if clientReplicator then
-- a RCCProfilerDataCompleteListener is mounted in DevConsoleMaster
-- to listen to and update the response of this call
clientReplicator:RequestRCCProfilerData(
GameSettings.RCCProfilerRecordFrameRate,
GameSettings.RCCProfilerRecordTimeFrame
)
self.props.dispatchSetRCCProfilerState(true)
end
end
self.changeProfilerState = function(screenProfilerEnabled)
return function()
GameSettings.OnScreenProfilerEnabled = screenProfilerEnabled
self:setState({
clientProfilerEnabled = screenProfilerEnabled
})
end
end
self.state = {
frameRate = GameSettings.RCCProfilerRecordFrameRate,
timeFrame = GameSettings.RCCProfilerRecordTimeFrame,
}
end
function ServerProfilerInteraface:render()
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local waitingForRecording = self.props.waitingForRecording
local lastFileOutputLocation = self.props.lastFileOutputLocation
local utilTabHeight = self.state.utilTabHeight
local frameRate = self.state.frameRate
local timeFrame = self.state.timeFrame
local clientProfilerEnabled = self.state.clientProfilerEnabled
local displayOutputFilePath = (not waitingForRecording) and #lastFileOutputLocation > 0
local textbox_size = UDim2.new(ROW_VALUE_WIDTH, -BUTTON_WIDTH, 0, ROW_HEIGHT)
local textbox_pos = UDim2.new(1 - ROW_VALUE_WIDTH / 2, BUTTON_WIDTH, 0, 0)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, ROW_HEIGHT * 4.5),
BackgroundTransparency = 1,
LayoutOrder = 4,
}, {
Label = Roact.createElement("TextLabel", {
Size = UDim2.new(0, BUTTON_WIDTH, 0, ROW_HEIGHT),
Position = UDim2.new(OFFSET, 0, 0, 0),
Text = "ServerProfiler",
Font = HEADER_FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = BUTTON_COLOR,
BackgroundTransparency = 1,
}),
HorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 0, ROW_HEIGHT),
}),
LabelFPS = Roact.createElement("TextLabel", {
Size = UDim2.new(0, BUTTON_WIDTH, 0, ROW_HEIGHT),
Position = UDim2.new(OFFSET, 0, 0, ROW_HEIGHT * 1.25),
Text = "Frames Per Second",
Font = FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = BUTTON_COLOR,
BackgroundTransparency = 1,
}),
FPSTextBox = Roact.createElement("TextBox", {
Size = textbox_size,
Position = UDim2.new((1 - ROW_VALUE_WIDTH)/2, BUTTON_WIDTH, 0, ROW_HEIGHT * 1.25),
Text = frameRate,
Font = FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = BUTTON_COLOR,
BackgroundTransparency = 0,
[Roact.Event.FocusLost] = self.onFocusLostFrameRate,
}),
LabelTimeFrame = Roact.createElement("TextLabel", {
Size = UDim2.new(0, BUTTON_WIDTH, 0, ROW_HEIGHT),
Position = UDim2.new(OFFSET, 0, 0, ROW_HEIGHT * 2.25),
Text = "Seconds to Record",
Font = FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundTransparency = 1,
}),
TimeFrameTextBox = Roact.createElement("TextBox", {
Size = textbox_size,
Position = UDim2.new((1 - ROW_VALUE_WIDTH)/2, BUTTON_WIDTH, 0, ROW_HEIGHT * 2.25),
Text = timeFrame,
Font = FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = BUTTON_COLOR,
BackgroundTransparency = 0,
[Roact.Event.FocusLost] = self.onFocusLostTimeFrame,
},{
-- placed here to better position this element
GetDumpButton = Roact.createElement("TextButton", {
Size = UDim2.new(0, BUTTON_WIDTH * .7, 0, ROW_HEIGHT),
Position = UDim2.new(1, -BUTTON_WIDTH * .7, 1, ROW_HEIGHT),
Text = waitingForRecording and "Recording" or "Start Recording",
Font = FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
BackgroundColor3 = waitingForRecording and BUTTON_UNSELECTED or BUTTON_SELECTED,
BackgroundTransparency = waitingForRecording and .3 or 0,
AutoButtonColor = not waitingForRecording,
Active = not waitingForRecording,
[Roact.Event.Activated] = self.requestRCCProfilerData,
})
}),
OutputPath = displayOutputFilePath and Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, ROW_HEIGHT),
Position = UDim2.new(0, 0, 0, ROW_HEIGHT * 3.25),
Text = lastFileOutputLocation,
Font = FONT,
TextSize = TEXT_SIZE,
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
})
})
end
local function mapStateToProps(state, props)
return {
waitingForRecording = state.MicroProfiler.waitingForRecording,
lastFileOutputLocation = state.MicroProfiler.lastFileOutputLocation,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchSetRCCProfilerState = function(waitingForRecording, fileLocation)
dispatch(SetRCCProfilerState(waitingForRecording, fileLocation))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(ServerProfilerInteraface)
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local ServerProfilerInterface = require(script.Parent.ServerProfilerInterface)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MicroProfiler = {
lastFileOutputLocation = ""
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
ServerProfilerInterface = Roact.createElement(ServerProfilerInterface, {
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,29 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Components = script.Parent.Parent.Parent.Components
local DataConsumer = require(Components.DataConsumer)
local NetworkView = require(script.Parent.NetworkView)
local ClientNetwork = Roact.Component:extend("ClientNetwork")
function ClientNetwork:init(props)
self.state = {
targetNetworkData = self.props.ClientNetworkData
}
end
function ClientNetwork:render()
local layoutOrder = self.props.layoutOrder
local searchTerm = self.props.searchTerm
local size = self.props.size
return Roact.createElement(NetworkView, {
size = size,
searchTerm = searchTerm,
layoutOrder = layoutOrder,
targetNetworkData = self.state.targetNetworkData
})
end
return DataConsumer(ClientNetwork, "ClientNetworkData")
@@ -0,0 +1,31 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local ClientNetwork = require(script.Parent.ClientNetwork)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
isDeveloperView = true,
},
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
ClientNetwork = Roact.createElement(ClientNetwork, {
size = UDim2.new(),
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,145 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Components = script.Parent.Parent.Parent.Components
local ClientNetwork = require(Components.Network.ClientNetwork)
local ServerNetwork = require(Components.Network.ServerNetwork)
local UtilAndTab = require(Components.UtilAndTab)
local Actions = script.Parent.Parent.Parent.Actions
local ClientNetworkUpdateSearchFilter = require(Actions.ClientNetworkUpdateSearchFilter)
local ServerNetworkUpdateSearchFilter = require(Actions.ServerNetworkUpdateSearchFilter)
local SetActiveTab = require(Actions.SetActiveTab)
local Constants = require(script.Parent.Parent.Parent.Constants)
local PADDING = Constants.GeneralFormatting.MainRowPadding
local MainViewNetwork = Roact.Component:extend("MainViewNetwork")
function MainViewNetwork:init()
self.onUtilTabHeightChanged = function(utilTabHeight)
self:setState({
utilTabHeight = utilTabHeight
})
end
self.onClientButton = function()
self.props.dispatchSetActiveTab("Network", true)
end
self.onServerButton = function()
self.props.dispatchSetActiveTab("Network", false)
end
self.onSearchTermChanged = function(newSearchTerm)
if self.props.isClientView then
self.props.dispatchClientNetworkUpdateSearchFilter(newSearchTerm, {})
else
self.props.dispatchServerNetworkUpdateSearchFilter(newSearchTerm, {})
end
end
self.utilRef = Roact.createRef()
self.state = {
utilTabHeight = 0,
}
end
function MainViewNetwork:didMount()
local utilSize = self.utilRef.current.Size
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
function MainViewNetwork:didUpdate()
local utilSize = self.utilRef.current.Size
if utilSize.Y.Offset ~= self.state.utilTabHeight then
self:setState({
utilTabHeight = utilSize.Y.Offset
})
end
end
function MainViewNetwork:render()
local elements = {}
local size = self.props.size
local formFactor = self.props.formFactor
local tabList = self.props.tabList
local isClientView = self.props.isClientView
local utilTabHeight = self.state.utilTabHeight
local searchTerm = isClientView and self.props.clientSearchTerm or self.props.serverSearchTerm
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, PADDING),
})
elements ["UtilAndTab"] = Roact.createElement(UtilAndTab, {
windowWidth = size.X.Offset,
formFactor = formFactor,
tabList = tabList,
isClientView = isClientView,
searchTerm = searchTerm,
layoutOrder = 1,
refForParent = self.utilRef,
onHeightChanged = self.onUtilTabHeightChanged,
onClientButton = self.onClientButton,
onServerButton = self.onServerButton,
onSearchTermChanged = self.onSearchTermChanged,
})
if utilTabHeight > 0 then
if isClientView then
elements["ClientNetwork"] = Roact.createElement(ClientNetwork, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
else
elements["ServerNetwork"] = Roact.createElement(ServerNetwork, {
size = UDim2.new(1, 0, 1, -utilTabHeight),
searchTerm = searchTerm,
layoutOrder = 2,
})
end
end
return Roact.createElement("Frame", {
Size = size,
BackgroundColor3 = Constants.Color.BaseGray,
BackgroundTransparency = 1,
LayoutOrder = 3,
}, elements)
end
local function mapStateToProps(state, props)
return {
isClientView = state.MainView.isClientView,
clientSearchTerm = state.NetworkData.clientSearchTerm,
serverSearchTerm = state.NetworkData.serverSearchTerm,
}
end
local function mapDispatchToProps(dispatch)
return {
dispatchClientNetworkUpdateSearchFilter = function(searchTerm, filters)
dispatch(ClientNetworkUpdateSearchFilter(searchTerm, filters))
end,
dispatchServerNetworkUpdateSearchFilter = function(searchTerm, filters)
dispatch(ServerNetworkUpdateSearchFilter(searchTerm, filters))
end,
dispatchSetActiveTab = function (tabListIndex, isClientView)
dispatch(SetActiveTab(tabListIndex, isClientView))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(MainViewNetwork)
@@ -0,0 +1,38 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Store = require(CorePackages.Rodux).Store
local DataProvider = require(script.Parent.Parent.DataProvider)
local MainViewNetwork = require(script.Parent.MainViewNetwork)
it("should create and destroy without errors", function()
local store = Store.new(function()
return {
MainView = {
currTabIndex = 0,
isDeveloperView = true,
},
NetworkData = {
clientSearchTerm = "",
serverSearchTerm = ""
}
}
end)
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
DataProvider = Roact.createElement(DataProvider, {}, {
MainViewNetwork = Roact.createElement(MainViewNetwork, {
size = UDim2.new(),
tabList = {},
})
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,280 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.NetworkFormatting.ChartHeaderNames
local CELL_WIDTHS = Constants.NetworkFormatting.ChartCellWidths
local CELL_PADDING = Constants.NetworkFormatting.CellPadding
local HEADER_HEIGHT = Constants.NetworkFormatting.HeaderFrameHeight
local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local FONT_SIZE = Constants.DefaultFontSize.MainWindow
local MAIN_FONT = Constants.Font.MainWindow
local RESPONSE_STR_TEXT_HEIGHT = Constants.NetworkFormatting.ResponseStrHeight
local RESPONSE_WIDTH_RATIO = Constants.NetworkFormatting.ResponseWidthRatio
local Components = script.Parent.Parent.Parent.Components
local HeaderButton = require(Components.HeaderButton)
local NetworkChartEntry = require(script.Parent.NetworkChartEntry)
local totalEntryWidth = 0
for _, cellWidth in pairs(CELL_WIDTHS) do
totalEntryWidth = totalEntryWidth + cellWidth
end
-- create table of offsets and sizes for each cell
-- each of the first 5 cells has a fixed size
local currOffset = 0
local cellOffset = {}
local headerCellSize = {}
local entryCellSize = {}
for _, cellWidth in ipairs(CELL_WIDTHS) do
table.insert(cellOffset,UDim2.new(0, currOffset + CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(0, cellWidth - CELL_PADDING, 0, ENTRY_HEIGHT))
currOffset = currOffset + cellWidth
end
-- cell 1-5 are defined widths,
-- cell 6 pads out the remaining width in the row
table.insert(cellOffset,UDim2.new(0, currOffset + CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(1, -totalEntryWidth - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(1, -totalEntryWidth - CELL_PADDING, 0, ENTRY_HEIGHT))
local verticalOffsets = {}
for i, offset in ipairs(cellOffset) do
verticalOffsets[i] = UDim2.new(offset.X.Scale, offset.X.Offset - CELL_PADDING,
offset.Y.Scale, offset.Y.Offset)
end
local NetworkChart = Roact.Component:extend("NetworkChart")
function NetworkChart:init()
self.getOnExpandEntry = function (name)
return function(rbx, input)
self:setState({
expandIndex = self.state.expandIndex ~= name and name
})
end
end
self.onCanvasPosChanged = function()
local canvasPos = self.scrollingRef.current.CanvasPosition
if self.state.canvasPos ~= canvasPos then
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = canvasPos,
})
end
end
self.ref = Roact.createRef()
self.scrollingRef = Roact.createRef()
self.state = {
expandIndex = false,
}
end
function NetworkChart:willUpdate()
if self.canvasPosConnector then
self.canvasPosConnector:Disconnect()
end
end
function NetworkChart:didUpdate()
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
local absScrollSize = self.scrollingRef.current.AbsoluteSize
if self.state.absScrollSize ~= absScrollSize then
self:setState({
absScrollSize = absScrollSize,
})
end
end
end
function NetworkChart:didMount()
if self.scrollingRef.current then
local signal = self.scrollingRef.current:GetPropertyChangedSignal("CanvasPosition")
self.canvasPosConnector = signal:Connect(self.onCanvasPosChanged)
self:setState({
absScrollSize = self.scrollingRef.current.AbsoluteSize,
canvasPos = self.scrollingRef.current.CanvasPosition,
})
end
end
function NetworkChart:render()
local httpEntryList = self.props.httpEntryList or {}
local chartHeight = self.props.chartHeight
local width = self.props.width
local searchTerm = self.props.searchTerm
local layoutOrder = self.props.layoutOrder
local reverseSort = self.props.reverseSort
local onSortChanged = self.props.onSortChanged
local expandIndex = self.state.expandIndex
local absScrollSize = self.state.absScrollSize
local canvasPos = self.state.canvasPos
local headerCells = {}
for ind, name in ipairs(HEADER_NAMES) do
headerCells[name] = Roact.createElement(HeaderButton, {
text = name,
size = headerCellSize[ind],
pos = cellOffset[ind],
sortfunction = onSortChanged,
})
end
for i = 2, #verticalOffsets do
local key = string.format("VerticalLine_%d",i)
headerCells[key] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 0, HEADER_HEIGHT),
Position = verticalOffsets[i],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
local entries = {}
local canvasHeight = 0
local paddingHeight = -1
local usedFrameSpace = 0
local totalEntries = #httpEntryList
local searchCount = 0
entries["UIListLayout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
if canvasPos and absScrollSize then
for ind, entry in ipairs(httpEntryList) do
local valid = true
if searchTerm ~= "" then
valid = string.find(entry.RequestType:lower(), searchTerm:lower()) ~= nil or
string.find(entry.Url:lower(), searchTerm:lower()) ~= nil
end
if not (entry.RequestType == "Default") and valid then
searchCount = searchCount + 1
-- insert header elements into a frame so that we can use the UIListLayout to keep everything in order
local showResponse = expandIndex == entry.Num
local frameHeight = ENTRY_HEIGHT
local responseBodyHeight = 0
if showResponse then
frameHeight = frameHeight + RESPONSE_STR_TEXT_HEIGHT
if self.ref.current then
local fSize = Vector2.new(
self.ref.current.AbsoluteSize.X * RESPONSE_WIDTH_RATIO,
100000000)
local DisSize = TextService:GetTextSize(entry.Response, FONT_SIZE, MAIN_FONT, fSize)
responseBodyHeight = RESPONSE_STR_TEXT_HEIGHT + DisSize.Y
frameHeight = frameHeight + responseBodyHeight
end
end
if canvasHeight + frameHeight >= canvasPos.Y then
if usedFrameSpace < absScrollSize.Y then
local layoutOrder = reverseSort and totalEntries - ind or ind
entries[ind] = Roact.createElement(NetworkChartEntry, {
size = UDim2.new(1, 0, 0, frameHeight),
entry = entry,
entryCellSize = entryCellSize,
cellOffset = cellOffset,
verticalOffsets = verticalOffsets,
showResponse = showResponse,
responseBodyHeight = responseBodyHeight,
layoutOrder = layoutOrder + 1,
onButtonPress = self.getOnExpandEntry(entry.Num),
})
end
if paddingHeight < 0 then
paddingHeight = canvasHeight
else
usedFrameSpace = usedFrameSpace + frameHeight
end
end
canvasHeight = canvasHeight + frameHeight
end
end
if searchCount > 0 then
entries["WindowingPadding"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, paddingHeight),
BackgroundTransparency = 1,
LayoutOrder = 1,
})
end
end
if totalEntries == 0 or searchCount == 0 then
entries["NONE FOUND"] = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, chartHeight),
Text = "No Network Entries Found",
TextColor3 = Constants.Color.Text,
BackgroundTransparency = 1,
})
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, chartHeight),
BackgroundTransparency = 1,
ClipsDescendants = true,
LayoutOrder = layoutOrder,
[Roact.Ref] = self.ref,
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Header = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = 1,
},headerCells),
HorizontalLine_1 = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
BackgroundTransparency = 0,
LayoutOrder = 2,
}),
scrollingFrameEntries = Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
CanvasSize = UDim2.new(0, width, 0, canvasHeight),
ScrollBarThickness = 6,
BackgroundTransparency = 1,
LayoutOrder = 3,
[Roact.Ref] = self.scrollingRef,
}, entries),
})
end
return NetworkChart
@@ -0,0 +1,14 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local NetworkChart = require(script.Parent.NetworkChart)
it("should create and destroy without errors", function()
local element = Roact.createElement(NetworkChart, {
summaryHeight = 0,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,111 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Parent.Constants)
local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local RESPONSE_STR_TEXT_HEIGHT = Constants.NetworkFormatting.ResponseStrHeight
local RESPONSE_WIDTH_RATIO = Constants.NetworkFormatting.ResponseWidthRatio
local RESPONSE_X_OFFSET = (1 - RESPONSE_WIDTH_RATIO) / 2
local RESPONSE_STR_X_OFFSET = (1 - RESPONSE_WIDTH_RATIO) / 4
local RESPONSE_STR_TEXT = "Response Body:"
local Components = script.Parent.Parent.Parent.Components
local CellLabel = require(Components.CellLabel)
local HeaderButton = require(Components.HeaderButton)
local BannerButton = require(Components.BannerButton)
return function(props)
local size = props.size
local entry = props.entry
local entryCellSize = props.entryCellSize
local cellOffset = props.cellOffset
local verticalOffsets = props.verticalOffsets
local layoutOrder = props.layoutOrder
local showResponse = props.showResponse
local responseBodyHeight = props.responseBodyHeight
local onButtonPress = props.onButtonPress
local row = {}
row["Num"] = Roact.createElement(CellLabel, {
text = entry.Num,
size = entryCellSize[1],
pos = cellOffset[1],
})
row["Method"] = Roact.createElement(CellLabel, {
text = entry.Method,
size = entryCellSize[2],
pos = cellOffset[2],
})
row["Status"] = Roact.createElement(CellLabel, {
text = entry.Status,
size = entryCellSize[3],
pos = cellOffset[3],
})
row["Time"] = Roact.createElement(CellLabel, {
text = string.format("%.3f", entry.Time),
size = entryCellSize[4],
pos = cellOffset[4],
})
row["RequestType"] = Roact.createElement(CellLabel, {
text = entry.RequestType,
size = entryCellSize[5],
pos = cellOffset[5],
})
row["Url"] = Roact.createElement(CellLabel, {
text = entry.Url,
size = entryCellSize[6],
pos = cellOffset[6],
})
row["LowerHorizontalLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
for i = 2, #verticalOffsets do
local key = string.format("VerticalLine_%d",i)
row[key] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 0, ENTRY_HEIGHT),
Position = verticalOffsets[i],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
-- to make room for the windowing padding
LayoutOrder = layoutOrder,
}, {
Button = Roact.createElement(BannerButton, {
size = UDim2.new(1, 0, 0, ENTRY_HEIGHT),
pos = UDim2.new(),
isExpanded = showResponse,
onButtonPress = onButtonPress,
}, row),
Response = showResponse and Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, responseBodyHeight),
Position = UDim2.new(RESPONSE_X_OFFSET, 0, 0, ENTRY_HEIGHT),
BackgroundTransparency = 1,
}, {
Text = Roact.createElement(HeaderButton, {
text = RESPONSE_STR_TEXT,
size = UDim2.new(1, 0, 0, RESPONSE_STR_TEXT_HEIGHT),
pos = UDim2.new(-RESPONSE_STR_X_OFFSET, 0, 0, 0),
}),
ResponseBody = Roact.createElement(CellLabel, {
text = entry.Response,
size = UDim2.new(.8, 0, 1, -RESPONSE_STR_TEXT_HEIGHT),
pos = UDim2.new(0, 0, 0, RESPONSE_STR_TEXT_HEIGHT),
})
})
})
end
@@ -0,0 +1,35 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local NetworkChartEntry = require(script.Parent.NetworkChartEntry)
it("should create and destroy without errors", function()
local dummyEntry = {
Num = 0,
Method = "",
Status = "",
Time = 0,
RequestType = "",
Url = "",
Reponse = "",
}
local dummyCellSizes = {
UDim2.new(),
UDim2.new(),
UDim2.new(),
UDim2.new(),
UDim2.new(),
UDim2.new(),
}
local element = Roact.createElement(NetworkChartEntry, {
entry = dummyEntry,
entryCellSize = dummyCellSizes,
cellOffset = dummyCellSizes,
verticalOffsets = dummyCellSizes,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,220 @@
local LogService = game:GetService("LogService")
local CircularBuffer = require(script.Parent.Parent.Parent.CircularBuffer)
local Signal = require(script.Parent.Parent.Parent.Signal)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.NetworkFormatting.ChartHeaderNames
local MAX_NUM_ENTRIES = tonumber(settings():GetFVariable("NewDevConsoleMaxHttpCount"))
local SORT_COMPARATOR = {
[HEADER_NAMES[1]] = function(a, b)
return a.Num < b.Num
end,
[HEADER_NAMES[2]] = function(a, b)
return a.Method < b.Method
end,
[HEADER_NAMES[3]] = function(a, b)
return a.Status < b.Status
end,
[HEADER_NAMES[4]] = function(a, b)
return a.Time < b.Time
end,
[HEADER_NAMES[5]] = function(a, b)
return a.RequestType < b.RequestType
end,
[HEADER_NAMES[6]] = function(a, b)
return a.Url < b.Url
end,
}
local function newHttpEntry(num, method, status, time, requestType, url, response)
return {
Num = num,
Method = method,
Status = status,
Time = time,
RequestType = requestType,
Url = url,
Response = response
}
end
local NetworkData = {}
NetworkData.__index = NetworkData
function NetworkData.new( isClient )
local self = {}
setmetatable(self, NetworkData)
self._isRunning = false
self._isClient = isClient
self._httpResultSignal = isClient and LogService.HttpResultOut or LogService.ServerHttpResultOut
self._httpEntryAdded = Signal.new()
self._httpSummaryTable = {}
self._httpSummaryCount = 0
self._httpEntryBuffer = CircularBuffer.new(MAX_NUM_ENTRIES)
self._httpLifeTimeEntryCount = 0
self._sortType = HEADER_NAMES[1] -- Num
return self
end
function NetworkData:addHttpEntry(httpResult)
-- append new entry to the back of the list
self._httpLifeTimeEntryCount = self._httpLifeTimeEntryCount + 1
local entry = newHttpEntry(
self._httpLifeTimeEntryCount,
httpResult.Method,
httpResult.Status,
httpResult.Time,
httpResult.RequestType,
httpResult.URL,
httpResult.Response
)
self._httpEntryBuffer:push_back(entry)
-- update Summary Data
local requestType = httpResult.RequestType
local summaryTable = self._httpSummaryTable[requestType]
if not summaryTable then
self._httpSummaryCount = self._httpSummaryCount + 1
self._httpSummaryTable[requestType] = {
RequestType = requestType,
RequestCount = 1,
FailedCount = 0,
AverageTime = httpResult.Time,
MinTime = httpResult.Time,
MaxTime = httpResult.Time,
}
if httpResult.Status >= 400 then
self._httpSummaryTable[httpResult.RequestType].FailedCount = 1
end
else
summaryTable.RequestCount = summaryTable.RequestCount + 1
if httpResult.Status >= 400 then
summaryTable.FailedCount = summaryTable.FailedCount + 1
end
summaryTable.AverageTime = ((summaryTable.AverageTime * summaryTable.RequestCount) +
httpResult.Time - summaryTable.AverageTime) / summaryTable.RequestCount
if httpResult.Time < summaryTable.MinTime then
summaryTable.MinTime = httpResult.Time
end
if httpResult.Time > summaryTable.MaxTime then
summaryTable.MaxTime = httpResult.Time
end
end
end
function NetworkData:setSortType(sortType)
if SORT_COMPARATOR[sortType] then
self._sortType = sortType
self._httpEntryAdded:Fire(
self._httpSummaryTable,
self._httpSummaryCount,
self:getSortedEntries()
)
else
error(string.format("attempted to pass invalid sortType: %s", tostring(sortType)), 2)
end
end
function NetworkData:getSortType()
return self._sortType
end
function NetworkData:resetHttpEntryList()
self._httpSummaryTable = {}
self._httpEntryBuffer:reset()
self._httpLifeTimeEntryCount = 0
if self._isClient then
local history = LogService:GetHttpResultHistory()
if history then
for _,httpResult in pairs(history) do
self:addHttpEntry(httpResult)
end
end
end
end
function NetworkData:Signal()
return self._httpEntryAdded
end
function NetworkData:getSortedEntries()
local iter = self._httpEntryBuffer:iterator()
local data = iter:next()
local entryList = {}
local count = 1
while data do
entryList[count] = data
count = count + 1
data = iter:next()
end
table.sort(entryList, SORT_COMPARATOR[self._sortType])
return entryList
end
function NetworkData:getCurrentData()
return {
summaryTable = self._httpSummaryTable,
summaryCount = self._httpSummaryCount,
entryList = self:getSortedEntries(),
}
end
function NetworkData:isRunning()
return self._isRunning
end
function NetworkData:start()
if not self._httpResultConnection then
if self._additionHttpSetup then
self._additionHttpSetup()
end
self._httpResultConnection = self._httpResultSignal:connect(function (httpResult)
self:addHttpEntry(httpResult)
self._httpEntryAdded:Fire(
self._httpSummaryTable,
self._httpSummaryCount,
self:getSortedEntries()
)
end)
self:resetHttpEntryList()
if not self._isClient then
self._onHttpResultApprovedConnection = LogService.OnHttpResultApproved:connect( function (isApproved)
LogService:RequestHttpResultApproved()
if isApproved then
self._onHttpResultApprovedConnection:Disconnect()
self._onHttpResultApprovedConnection = nil
end
end)
LogService:RequestServerHttpResult()
end
self._isRunning = true
end
end
function NetworkData:stop()
if self._httpResultConnection then
self._httpResultConnection:Disconnect()
self._httpResultConnection = nil
end
self._isRunning = false
end
return NetworkData
@@ -0,0 +1,193 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_NAMES = Constants.NetworkFormatting.SummaryHeaderNames
local CELL_WIDTH = Constants.NetworkFormatting.SummaryCellWidths
local HEADER_HEIGHT = Constants.NetworkFormatting.HeaderFrameHeight
local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight
local LINE_WIDTH = Constants.GeneralFormatting.LineWidth
local LINE_COLOR = Constants.GeneralFormatting.LineColor
local CELL_PADDING = Constants.NetworkFormatting.CellPadding
local TEXT_COLOR = Constants.Color.Text
local NON_DATA_STR = "No Summary Data Found"
local CellLabel = require(script.Parent.Parent.Parent.Components.CellLabel)
local HeaderButton = require(script.Parent.Parent.Parent.Components.HeaderButton)
local totalSummaryWidth = 0
for _, v in pairs(CELL_WIDTH) do
totalSummaryWidth = totalSummaryWidth + v
end
-- create table of offsets and sizes for each cell
local currOffset = -totalSummaryWidth
local cellOffset = {}
local headerCellSize = {}
local entryCellSize = {}
table.insert(cellOffset, UDim2.new(0, CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(1, -totalSummaryWidth - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(1, -totalSummaryWidth - CELL_PADDING, 0, ENTRY_HEIGHT))
for _, width in ipairs(CELL_WIDTH) do
table.insert(cellOffset,UDim2.new(1, currOffset + CELL_PADDING, 0, 0))
table.insert(headerCellSize, UDim2.new(0, width - CELL_PADDING, 0, HEADER_HEIGHT))
table.insert(entryCellSize, UDim2.new(0, width - CELL_PADDING, 0, ENTRY_HEIGHT))
currOffset = currOffset + width
end
local verticalOffsets = {}
for i, offset in ipairs(cellOffset) do
verticalOffsets[i] = UDim2.new(
offset.X.Scale,
offset.X.Offset - CELL_PADDING,
offset.Y.Scale,
offset.Y.Offset
)
end
local NetworkSummary = Roact.Component:extend("NetworkSummary")
function NetworkSummary:render()
local width = self.props.width
local httpSummaryTable = self.props.httpSummaryTable or {}
local layoutOrder = self.props.layoutOrder
local elements = {}
elements["UIListLayout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
})
local summaryHeader = {}
summaryHeader["UpperHorizontalLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
summaryHeader["LowerHorizontalLine"] = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
for i = 2, #verticalOffsets do
local key = string.format("VerticalLine_%d",i)
summaryHeader[key] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 0, HEADER_HEIGHT),
Position = verticalOffsets[i],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
for ind, name in ipairs(HEADER_NAMES) do
summaryHeader[name] = Roact.createElement(HeaderButton, {
text = name,
size = headerCellSize[ind],
pos = cellOffset[ind],
})
end
elements["Header"] = Roact.createElement("Frame", {
Size = UDim2.new(0, width, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = 1,
}, summaryHeader)
local entryCount = 0
for _, dataEntry in pairs(httpSummaryTable) do
entryCount = entryCount + 1
local row = {}
row.RequestType = Roact.createElement(CellLabel, {
text = dataEntry.RequestType,
size = headerCellSize[1],
pos = cellOffset[1],
})
row.RequestCount = Roact.createElement(CellLabel, {
text = dataEntry.RequestCount,
size = headerCellSize[2],
pos = cellOffset[2],
})
row.FailedCount = Roact.createElement(CellLabel, {
text = dataEntry.FailedCount,
size = headerCellSize[3],
pos = cellOffset[3],
})
row.AverageTime = Roact.createElement(CellLabel, {
text = string.format("%.3f", dataEntry.AverageTime),
size = headerCellSize[4],
pos = cellOffset[4],
})
row.MinTime = Roact.createElement(CellLabel, {
text = string.format("%.3f", dataEntry.MinTime),
size = headerCellSize[5],
pos = cellOffset[5],
})
row.MaxTime = Roact.createElement(CellLabel, {
text = string.format("%.3f", dataEntry.MaxTime),
size = headerCellSize[6],
pos = cellOffset[6],
})
row.LowerHorizontalLine = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, LINE_WIDTH),
Position = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
for ind = 2, #verticalOffsets do
local key = string.format("VerticalLine_%d",ind)
row[key] = Roact.createElement("Frame", {
Size = UDim2.new(0, LINE_WIDTH, 1, 0),
Position = verticalOffsets[ind],
BackgroundColor3 = LINE_COLOR,
BorderSizePixel = 0,
})
end
elements[dataEntry.RequestType] = Roact.createElement("Frame", {
Size = UDim2.new(0, width, 0, ENTRY_HEIGHT),
BackgroundTransparency = 1,
LayoutOrder = entryCount + 1
},row)
end
if entryCount == 0 then
elements["Padding"] = Roact.createElement("TextLabel", {
Size = UDim2.new(0, width, 0, ENTRY_HEIGHT),
Text = NON_DATA_STR,
TextColor3 = TEXT_COLOR,
BackgroundTransparency = 1,
LayoutOrder = 2
})
entryCount = 1
end
local summaryHeight = entryCount * ENTRY_HEIGHT + HEADER_HEIGHT
-- update offsets to remove padding so we can use for vertical line
return Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, 0, 0, summaryHeight),
ScrollingEnabled = false,
ScrollBarThickness = 0,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, elements)
end
return NetworkSummary
@@ -0,0 +1,12 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local NetworkSummary = require(script.Parent.NetworkSummary)
it("should create and destroy without errors", function()
local element = Roact.createElement(NetworkSummary)
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,197 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Constants = require(script.Parent.Parent.Parent.Constants)
local HEADER_HEIGHT = Constants.NetworkFormatting.HeaderFrameHeight
local ENTRY_HEIGHT = Constants.NetworkFormatting.EntryFrameHeight
local SUMMARY_HEIGHT = Constants.NetworkFormatting.SummaryButtonHeight
local MIN_WIDTH = Constants.NetworkFormatting.MinFrameWidth
local BANNER_FONT_SIZE = Constants.DefaultFontSize.MainWindow
local BANNER_FONT = Constants.Font.MainWindowHeader
local INDENT = 30
local Components = script.Parent.Parent.Parent.Components
local BannerButton = require(Components.BannerButton)
local NetworkSummary = require(Components.Network.NetworkSummary)
local NetworkChart = require(Components.Network.NetworkChart)
local NetworkView = Roact.Component:extend("NetworkView")
function NetworkView:init(props)
assert(props.targetNetworkData, "Make sure the NetworkData is assigned for this NetworkView")
local currentData = props.targetNetworkData:getCurrentData()
self.onSortChanged = function(sortType)
local currSortType = props.targetNetworkData:getSortType()
if sortType == currSortType then
self:setState({
reverseSort = not self.state.reverseSort
})
else
props.targetNetworkData:setSortType(sortType)
self:setState({
reverseSort = false,
})
end
end
self.onSummaryButton = function(rbx, input)
self:setState({
summaryExpanded = not self.state.summaryExpanded,
})
end
self.onDetailButton = function(rbx, input)
self:setState({
entriesExpanded = not self.state.entriesExpanded,
})
end
self.ref = Roact.createRef()
self.state = {
httpSummaryTable = currentData.summaryTable,
httpSummaryCount = currentData.summaryCount,
httpEntryList = currentData.entryList,
summaryExpanded = true,
entriesExpanded = true,
indexOfEntryExpanded = 0,
reverseSort = false,
absWidth = 0,
}
end
function NetworkView:didUpdate()
if self.ref.current then
if self.state.absWidth ~= self.ref.current.AbsoluteSize.X then
self:setState({
absWidth = self.ref.current.AbsoluteSize.X,
})
end
end
end
function NetworkView:didMount()
local networkDataSignal = self.props.targetNetworkData:Signal()
self.httpEntryAddedConnector = networkDataSignal:Connect(function(summaryTable, summaryCount, entryList)
self:setState({
httpSummaryTable = summaryTable,
httpSummaryCount = summaryCount,
httpEntryList = entryList,
})
end)
if self.ref.current then
self:setState({
absWidth = self.ref.current.AbsoluteSize.X,
})
end
end
function NetworkView:willUnmount()
self.httpEntryAddedConnector:Disconnect()
self.httpEntryAddedConnector = nil
end
function NetworkView:render()
local layoutOrder = self.props.layoutOrder
local searchTerm = self.props.searchTerm
local size = self.props.size
local summaryExpanded = self.state.summaryExpanded
local entriesExpanded = self.state.entriesExpanded
local reverseSort = self.state.reverseSort
local httpSummaryTable = self.state.httpSummaryTable
local httpEntryList = self.state.httpEntryList
local httpSummaryCount = self.state.httpSummaryCount
local absWidth = math.max(MIN_WIDTH, self.state.absWidth)
-- for both the detail button and summary button
local summaryHeight = SUMMARY_HEIGHT * 2
if summaryExpanded then
summaryHeight = summaryHeight + httpSummaryCount * ENTRY_HEIGHT + HEADER_HEIGHT
end
local chartHeight
if self.ref.current then
chartHeight = self.ref.current.AbsoluteSize.Y - summaryHeight
else
chartHeight = size.Y.Offset - summaryHeight
end
chartHeight = math.max(0, chartHeight)
return Roact.createElement("Frame", {
Size = size,
BackgroundTransparency = 1,
ClipsDescendants = true,
LayoutOrder = layoutOrder,
[Roact.Ref] = self.ref,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
SummaryButton = Roact.createElement(BannerButton, {
size = UDim2.new(1, 0, 0, SUMMARY_HEIGHT),
pos = UDim2.new(),
isExpanded = summaryExpanded,
onButtonPress = self.onSummaryButton,
layoutOrder = 1,
}, {
Text = Roact.createElement("TextLabel", {
Text = "Summary",
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = BANNER_FONT_SIZE,
Font = BANNER_FONT,
Size = UDim2.new(1,-INDENT,0,SUMMARY_HEIGHT),
Position = UDim2.new(0, INDENT, 0, 0),
BackgroundTransparency = 1,
})
}),
Summary = summaryExpanded and Roact.createElement(NetworkSummary, {
width = absWidth,
httpSummaryTable = httpSummaryTable,
layoutOrder = 2,
}),
DetailsButton = Roact.createElement(BannerButton, {
size = UDim2.new(1, 0, 0, SUMMARY_HEIGHT),
pos = UDim2.new(),
isExpanded = entriesExpanded,
onButtonPress = self.onDetailButton,
layoutOrder = 3,
}, {
Text = Roact.createElement("TextLabel", {
Text = "Details",
TextColor3 = Constants.Color.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = BANNER_FONT_SIZE,
Font = BANNER_FONT,
Size = UDim2.new(1,-INDENT,0,SUMMARY_HEIGHT),
Position = UDim2.new(0, INDENT, 0, 0),
BackgroundTransparency = 1,
})
}),
Entries = entriesExpanded and Roact.createElement(NetworkChart, {
httpEntryList = httpEntryList,
chartHeight = chartHeight,
width = absWidth,
searchTerm = searchTerm,
reverseSort = reverseSort,
layoutOrder = 4,
onSortChanged = self.onSortChanged
})
})
end
return NetworkView

Some files were not shown because too many files have changed in this diff Show More